From 6478951e603e68f44794a77fcb64358e4062dbe1 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Tue, 17 Dec 2024 07:58:31 +0100 Subject: [PATCH 01/20] ci: update macOS runners to macos-14 --- .github/BUILD.bazel | 2 +- .github/workflows/prerelease.yaml | 2 +- .github/workflows/run-all-tests-main.yml | 4 ++-- .github/workflows/run-all-tests-pr.yml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/BUILD.bazel b/.github/BUILD.bazel index 4a3a2d889..8b0c07fc1 100644 --- a/.github/BUILD.bazel +++ b/.github/BUILD.bazel @@ -20,7 +20,7 @@ xcode_version( version = "14.2.0.14C18", ) -# Xcode version on public GitHub Actions macos-13 runners +# Xcode version on public GitHub Actions macos-13 and macos-14 runners xcode_version( name = "version15_2_0_15C500b", aliases = [ diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index 936030334..3f004a57c 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -14,7 +14,7 @@ jobs: include: - os: ubuntu-22.04 name: linux - - os: macos-13 + - os: macos-14 name: macos - os: windows-2019 name: windows diff --git a/.github/workflows/run-all-tests-main.yml b/.github/workflows/run-all-tests-main.yml index 43e43c672..072b72388 100644 --- a/.github/workflows/run-all-tests-main.yml +++ b/.github/workflows/run-all-tests-main.yml @@ -36,11 +36,11 @@ jobs: name: Build & Test strategy: matrix: - os: [ macos-13, windows-2019 ] + os: [ macos-14, windows-2019 ] # Test JDK 8 on Windows and mac only on main. jdk: [8] include: - - os: macos-13 + - os: macos-14 arch: "macos-arm64" bazel_args: "--xcode_version_config=//.github:host_xcodes" - os: windows-2019 diff --git a/.github/workflows/run-all-tests-pr.yml b/.github/workflows/run-all-tests-pr.yml index b068c7bbd..fcbfc725c 100644 --- a/.github/workflows/run-all-tests-pr.yml +++ b/.github/workflows/run-all-tests-pr.yml @@ -17,7 +17,7 @@ jobs: name: Build & Test strategy: matrix: - os: [ubuntu-22.04, windows-2019, macos-13] + os: [ubuntu-22.04, windows-2019, macos-14] jdk: [21] include: - jdk: 21 @@ -29,7 +29,7 @@ jobs: - os: ubuntu-22.04 # Use JDK 8 only on Ubuntu in PRs. jdk: 8 - - os: macos-13 + - os: macos-14 arch: "macos-arm64" bazel_args: "--xcode_version_config=//.github:host_xcodes" - os: windows-2019 From 60bbf88db1c593249374d32b2c8d35808f0b7d00 Mon Sep 17 00:00:00 2001 From: Robert Czechowski Date: Wed, 18 Dec 2024 09:58:16 +0100 Subject: [PATCH 02/20] ci: Update softprops/action-gh-release to v2.2.0 in prerelease workflow --- .github/workflows/prerelease.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index 3f004a57c..ff1fc1131 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -143,7 +143,7 @@ jobs: path: _releases/ - name: create release - uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981 + uses: softprops/action-gh-release@v2.2.0 with: generate_release_notes: true draft: true From 76f061ad23af425af243673bb631f4342ecc0e0c Mon Sep 17 00:00:00 2001 From: Robert Czechowski Date: Wed, 18 Dec 2024 11:21:35 +0100 Subject: [PATCH 03/20] ci: Downgrade softprops/action-gh-release to v2.0.9 in prerelease workflow as v2.2.0 is broken --- .github/workflows/prerelease.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml index ff1fc1131..d3297dc07 100644 --- a/.github/workflows/prerelease.yaml +++ b/.github/workflows/prerelease.yaml @@ -143,7 +143,7 @@ jobs: path: _releases/ - name: create release - uses: softprops/action-gh-release@v2.2.0 + uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9 with: generate_release_notes: true draft: true From 11b42852df4344737df54a380c2f522025bb4e84 Mon Sep 17 00:00:00 2001 From: Robert Czechowski Date: Wed, 18 Dec 2024 11:28:14 +0100 Subject: [PATCH 04/20] ci: Automatically run release pipeline when github draft release is released This releases the release artifacts and deploys the documentation. --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 450ed95f6..d4f014a95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,9 @@ name: Release on: workflow_dispatch: + release: + types: [released] + jobs: From 90bbaafe6d655acaabb4650427046d4cec4d0801 Mon Sep 17 00:00:00 2001 From: Robert Czechowski Date: Fri, 20 Dec 2024 08:53:09 +0100 Subject: [PATCH 05/20] maven: Add license and developer information to maven medatada POM --- deploy/jazzer-api.pom | 21 +++++++++++++++++++++ deploy/jazzer-junit.pom | 21 +++++++++++++++++++++ deploy/jazzer.pom | 21 +++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/deploy/jazzer-api.pom b/deploy/jazzer-api.pom index 8c72e803c..71a0a4e4e 100644 --- a/deploy/jazzer-api.pom +++ b/deploy/jazzer-api.pom @@ -38,4 +38,25 @@ https://github.com/CodeIntelligenceTesting/jazzer + + + + Apache-2.0 + + + + + + Fabian Meumertzheim + + + Norbert Schneider + + + Khaled Yakdan + + + Peter Samarin + + diff --git a/deploy/jazzer-junit.pom b/deploy/jazzer-junit.pom index bf7f30eef..f22e78c88 100644 --- a/deploy/jazzer-junit.pom +++ b/deploy/jazzer-junit.pom @@ -37,4 +37,25 @@ https://github.com/CodeIntelligenceTesting/jazzer + + + + Apache-2.0 + + + + + + Fabian Meumertzheim + + + Norbert Schneider + + + Khaled Yakdan + + + Peter Samarin + + diff --git a/deploy/jazzer.pom b/deploy/jazzer.pom index 6f85485a8..d86a3c430 100644 --- a/deploy/jazzer.pom +++ b/deploy/jazzer.pom @@ -37,4 +37,25 @@ https://github.com/CodeIntelligenceTesting/jazzer + + + + Apache-2.0 + + + + + + Fabian Meumertzheim + + + Norbert Schneider + + + Khaled Yakdan + + + Peter Samarin + + From 51c63c5220276435e17b1cdcf195e42ae5e74fa0 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Fri, 20 Dec 2024 10:52:19 +0100 Subject: [PATCH 06/20] maven: include docs and srcs for jazzer-junit and jazzer-api --- deploy/BUILD.bazel | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/deploy/BUILD.bazel b/deploy/BUILD.bazel index 99b9270fe..1567d999f 100644 --- a/deploy/BUILD.bazel +++ b/deploy/BUILD.bazel @@ -38,7 +38,6 @@ java_export( ], maven_coordinates = "com.code-intelligence:jazzer-api:$(JAZZER_VERSION)", pom_template = "//deploy:jazzer-api.pom", - tags = ["no-sources"], toolchains = [":jazzer_version"], visibility = ["//visibility:public"], runtime_deps = ["//src/main/java/com/code_intelligence/jazzer/api"], @@ -73,9 +72,14 @@ alias( java_export( name = "jazzer-junit", - # Exclude the unshaded classes comprising com.code-intelligence:jazzer since the java_library - # target comprising jazzer-junit depend on the individual libraries, not the shaded jar. - deploy_env = ["//src/main/java/com/code_intelligence/jazzer:jazzer_lib"], + deploy_env = [ + # Exclude the unshaded classes comprising com.code-intelligence:jazzer since the java_library + # target comprising jazzer-junit depend on the individual libraries, not the shaded jar. + "//src/main/java/com/code_intelligence/jazzer:jazzer_lib", + # Spring dependencies are required for javadoc but should be excluded from the jar. + "@maven//:org_springframework_spring_test", + "@maven//:org_springframework_spring_web", + ], doc_deps = [ ":jazzer-api-docs", ":jazzer-docs", @@ -89,12 +93,6 @@ java_export( ], maven_coordinates = "com.code-intelligence:jazzer-junit:$(JAZZER_VERSION)", pom_template = "jazzer-junit.pom", - tags = [ - "no-sources", - # Generating javadocs breaks the build due to weird dependency issues. - # Deactivate it for now. - "no-javadocs", - ], toolchains = [":jazzer_version"], visibility = ["//visibility:public"], runtime_deps = [ From d63ffa7a0cbef6defe71ab0340e6cfc57c6be9af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:22:46 +0000 Subject: [PATCH 07/20] chore(deps): bump com.google.protobuf:protobuf-java in /selffuzz Bumps [com.google.protobuf:protobuf-java](https://github.com/protocolbuffers/protobuf) from 3.25.2 to 3.25.5. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.25.2...v3.25.5) --- updated-dependencies: - dependency-name: com.google.protobuf:protobuf-java dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- selffuzz/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selffuzz/pom.xml b/selffuzz/pom.xml index 242b7e2a1..9d51893e4 100644 --- a/selffuzz/pom.xml +++ b/selffuzz/pom.xml @@ -140,7 +140,7 @@ com.google.protobuf protobuf-java - 3.25.2 + 3.25.5 From ed5d042e02b073bf8af4ec42edd367fef61bb320 Mon Sep 17 00:00:00 2001 From: Khaled Yakdan Date: Thu, 16 Jan 2025 10:04:06 +0100 Subject: [PATCH 08/20] readme: remove obsolete note regarding the old license --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 1cca99c6e..1259e7b3f 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,6 @@ See [the README](https://github.com/bazelbuild/rules_fuzzing#java-fuzzing) for i [Code Intelligence](https://code-intelligence.com) and Google have teamed up to bring support for Java, Kotlin, and other JVM-based languages to [OSS-Fuzz](https://github.com/google/oss-fuzz), Google's project for large-scale fuzzing of open-source software. Read [the OSS-Fuzz guide](https://google.github.io/oss-fuzz/getting-started/new-project-guide/jvm-lang/) to learn how to set up a Java project. -**Note**: Open source projects can use Jazzer for free and benefit from the -OSS-Fuzz infrastructure, including ClusterFuzzLite and OSS-Fuzz-Gen for -automated analysis and continuous integration. There is no risk of accidental -license violation as long as Jazzer is used for testing open-source code. - ## Building from source Information on building and testing Jazzer for development can be found in [CONTRIBUTING.md](CONTRIBUTING.md) From 2880e2ff25f244bb417fa283201eeab384fce74e Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Wed, 15 Jan 2025 13:18:51 +0100 Subject: [PATCH 09/20] mutation: Add support for sealed classes --- .../combinator/MutatorCombinators.java | 97 +++++++++++++++++ .../jazzer/mutation/mutator/Mutators.java | 5 +- .../mutator/aggregate/AggregateMutators.java | 52 +++++---- .../mutation/mutator/aggregate/BUILD.bazel | 16 +++ .../aggregate/SealedClassMutatorFactory.java | 64 +++++++++++ .../jazzer/mutation/support/TypeSupport.java | 60 +++++++++++ .../jazzer/mutation/mutator/StressTest.java | 102 +++++++++++++++++- 7 files changed, 371 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java 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 index 567194435..7e8a95305 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/combinator/MutatorCombinators.java @@ -29,6 +29,7 @@ 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.errorprone.annotations.ImmutableTypeParameter; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -428,6 +429,102 @@ public String toDebugString(Predicate isInCycle) { }; } + /** + * Mutates a sum type (e.g. a sealed interface), 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 + */ + @SafeVarargs + public static SerializingMutator mutateSum( + ToIntFunction getState, SerializingMutator... perStateMutators) { + Preconditions.require(perStateMutators.length > 0, "At least one mutator must be provided"); + if (perStateMutators.length == 1) { + return perStateMutators[0]; + } + boolean hasFixedSize = stream(perStateMutators).allMatch(SerializingMutator::hasFixedSize); + final SerializingMutator[] mutators = + Arrays.copyOf(perStateMutators, perStateMutators.length); + return new SerializingMutator() { + @Override + public T init(PseudoRandom prng) { + return mutators[prng.indexIn(mutators)].init(prng); + } + + @Override + public T mutate(T value, PseudoRandom prng) { + int currentState = getState.applyAsInt(value); + if (currentState == -1) { + // The value is in an indeterminate state, initialize it. + return init(prng); + } + if (prng.trueInOneOutOf(100)) { + // Initialize to a different state. + return mutators[prng.otherIndexIn(mutators, currentState)].init(prng); + } + // Mutate within the current state. + return mutators[currentState].mutate(value, prng); + } + + @Override + public T crossOver(T value, T otherValue, PseudoRandom prng) { + // Try to cross over in current state and leave state changes to the mutate step. + int currentState = getState.applyAsInt(value); + int otherState = getState.applyAsInt(otherValue); + 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 value; + } + return mutators[otherState].init(prng); + } + if (currentState == otherState) { + return mutators[currentState].crossOver(value, otherValue, prng); + } + return value; + } + + @Override + public T detach(T value) { + int currentState = getState.applyAsInt(value); + if (currentState == -1) { + return value; + } + return mutators[currentState].detach(value); + } + + @Override + public T read(DataInputStream in) throws IOException { + int currentState = Math.floorMod(in.readInt(), mutators.length); + return mutators[currentState].read(in); + } + + @Override + public void write(T value, DataOutputStream out) throws IOException { + int currentState = getState.applyAsInt(value); + out.writeInt(currentState); + mutators[currentState].write(value, out); + } + + @Override + public boolean hasFixedSize() { + return hasFixedSize; + } + + @Override + public String toDebugString(Predicate isInCycle) { + return stream(mutators) + .map(mutator -> mutator.toDebugString(isInCycle)) + .collect(joining(" | ", "(", ")")); + } + }; + } + /** * Use {@link #markAsRequiringRecursionBreaking(SerializingMutator)} instead for {@link * com.code_intelligence.jazzer.mutation.api.ValueMutator}. 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 index 381e47f95..c0b810395 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/Mutators.java @@ -40,8 +40,9 @@ public static ExtendedMutatorFactory newFactory() { CollectionMutators.newFactories(), ProtoMutators.newFactories(), LibFuzzerMutators.newFactories(), - AggregateMutators.newFactories(), - TimeMutators.newFactories()); + TimeMutators.newFactories(), + // Keep generic aggregate mutators last in case a concrete type is also an aggregate type. + AggregateMutators.newFactories()); } // Mutators for which the NullableMutatorFactory diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java index bd65b35c1..854d4a04d 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/AggregateMutators.java @@ -25,26 +25,43 @@ private AggregateMutators() {} public static Stream newFactories() { // Register the record mutator first as it is more specific. - return Stream.concat( - newRecordMutatorFactoryIfSupported(), - Stream.of( - new SetterBasedBeanMutatorFactory(), - new ConstructorBasedBeanMutatorFactory(), - new CachedConstructorMutatorFactory())); + return Stream.of( + newRecordMutatorFactoryIfSupported(), + newSealedClassMutatorFactoryIfSupported(), + Stream.of( + new SetterBasedBeanMutatorFactory(), + new ConstructorBasedBeanMutatorFactory(), + new CachedConstructorMutatorFactory())) + .flatMap(s -> s); } private static Stream newRecordMutatorFactoryIfSupported() { - if (!supportsRecords()) { + try { + Class.forName("java.lang.Record"); + return Stream.of(instantiateMutatorFactory("RecordMutatorFactory")); + } catch (ClassNotFoundException ignored) { + return Stream.empty(); + } + } + + private static Stream newSealedClassMutatorFactoryIfSupported() { + try { + Class.class.getMethod("getPermittedSubclasses"); + return Stream.of(instantiateMutatorFactory("SealedClassMutatorFactory")); + } catch (NoSuchMethodException e) { return Stream.empty(); } + } + + private static MutatorFactory instantiateMutatorFactory(String simpleClassName) { try { - // Instantiate RecordMutatorFactory via reflection as making it a compile time dependency - // breaks the r8 step in the Android build. - Class recordMutatorFactory; - recordMutatorFactory = - Class.forName(AggregateMutators.class.getPackage().getName() + ".RecordMutatorFactory") + // Instantiate factory via reflection as making it a compile time dependency breaks the r8 + // step in the Android build. + Class factory; + factory = + Class.forName(AggregateMutators.class.getPackage().getName() + "." + simpleClassName) .asSubclass(MutatorFactory.class); - return Stream.of(recordMutatorFactory.getDeclaredConstructor().newInstance()); + return factory.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException @@ -53,13 +70,4 @@ private static Stream newRecordMutatorFactoryIfSupported() { throw new IllegalStateException(e); } } - - private static boolean supportsRecords() { - try { - Class.forName("java.lang.Record"); - return true; - } catch (ClassNotFoundException ignored) { - return false; - } - } } diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel index 81d18d401..436b8244c 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/BUILD.bazel @@ -6,6 +6,7 @@ java_library( "AggregatesHelper.java", "BeanSupport.java", "RecordMutatorFactory.java", + "SealedClassMutatorFactory.java", ], ), visibility = [ @@ -16,6 +17,7 @@ java_library( "@platforms//os:android": [], "//conditions:default": [ ":record_mutator_factory", + ":sealed_class_mutator_factory", ], }), deps = [ @@ -40,6 +42,20 @@ java_library( ], ) +java_library( + name = "sealed_class_mutator_factory", + srcs = ["SealedClassMutatorFactory.java"], + javacopts = [ + "--release", + "17", + ], + deps = [ + "//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", + ], +) + java_library( name = "aggregates_helper", srcs = [ diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java new file mode 100644 index 000000000..6c45e3891 --- /dev/null +++ b/src/main/java/com/code_intelligence/jazzer/mutation/mutator/aggregate/SealedClassMutatorFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 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.aggregate; + +import static com.code_intelligence.jazzer.mutation.support.StreamSupport.toArrayOrEmpty; +import static java.util.Arrays.stream; + +import com.code_intelligence.jazzer.mutation.api.ExtendedMutatorFactory; +import com.code_intelligence.jazzer.mutation.api.MutatorFactory; +import com.code_intelligence.jazzer.mutation.api.SerializingMutator; +import com.code_intelligence.jazzer.mutation.combinator.MutatorCombinators; +import com.code_intelligence.jazzer.mutation.support.TypeSupport; +import java.lang.reflect.AnnotatedType; +import java.util.Optional; +import java.util.function.ToIntFunction; + +final class SealedClassMutatorFactory implements MutatorFactory { + @Override + public Optional> tryCreate( + AnnotatedType type, ExtendedMutatorFactory factory) { + if (!(type.getType() instanceof Class)) { + return Optional.empty(); + } + Class[] permittedSubclasses = + (Class[]) ((Class) type.getType()).getPermittedSubclasses(); + if (permittedSubclasses == null) { + return Optional.empty(); + } + + ToIntFunction getState = + (value) -> { + // We can't use value.getClass() as it might be a subclass of the permitted (direct) + // subclasses. + for (int i = 0; i < permittedSubclasses.length; i++) { + if (permittedSubclasses[i].isInstance(value)) { + return i; + } + } + return -1; + }; + return toArrayOrEmpty( + stream(permittedSubclasses) + .map(TypeSupport::asAnnotatedType) + .map(TypeSupport::notNull) + .map(factory::tryCreate), + SerializingMutator[]::new) + .map( + mutators -> MutatorCombinators.mutateSum(getState, (SerializingMutator[]) mutators)); + } +} 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 index 5bdb8aa13..2480d4e93 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/support/TypeSupport.java @@ -29,6 +29,7 @@ import com.code_intelligence.jazzer.mutation.annotation.WithLength; import com.code_intelligence.jazzer.mutation.utils.PropertyConstraint; 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; @@ -94,6 +95,65 @@ public static Optional> asSubclassOrEmpty( return Optional.of(actualClazz.asSubclass(superclass)); } + /** + * Synthesizes an {@link AnnotatedType} for the given {@link Class}. + * + *

Usage of this method should be avoided in favor of obtaining annotated types in a natural + * way if possible (e.g. prefer {@link Class#getAnnotatedSuperclass()} to {@link + * Class#getSuperclass()}. + */ + public static AnnotatedType asAnnotatedType(Class clazz) { + requireNonNull(clazz); + return new AnnotatedType() { + @Override + public Type getType() { + return clazz; + } + + @Override + public T getAnnotation(Class 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( + annotation -> + annotation.annotationType().getDeclaredAnnotation(Inherited.class) != null) + .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. diff --git a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java index cb01c1e27..5c59ed8ad 100644 --- a/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java +++ b/src/test/java/com/code_intelligence/jazzer/mutation/mutator/StressTest.java @@ -124,6 +124,82 @@ private record RepeatedRecord(SimpleRecord first, SimpleRecord second) {} private record LinkedListNode(SimpleRecord value, LinkedListNode next) {} + private sealed interface Sealed { + sealed interface A extends Sealed { + record A1(@NotNull boolean b) implements A {} + } + + abstract sealed class B implements Sealed { + static final class B1 extends B { + private final boolean b; + + B1(boolean b) { + this.b = b; + } + + @NotNull + public boolean b() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B1 b1 = (B1) o; + return b == b1.b; + } + + @Override + public int hashCode() { + return Objects.hash(b); + } + + @Override + public String toString() { + return "B1{" + "b=" + b + '}'; + } + } + + static final class B2 extends B { + private final boolean b; + + B2(boolean b) { + this.b = b; + } + + @NotNull + public boolean b() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + B2 b1 = (B2) o; + return b == b1.b; + } + + @Override + public int hashCode() { + return Objects.hash(b); + } + + @Override + public String toString() { + return "B2{" + "b=" + b + '}'; + } + } + } + + sealed interface C extends Sealed { + record C1(@NotNull boolean b) implements C {} + + record C2(@NotNull int i) implements C {} + } + } + public static class SomeSetterBasedBean { protected long quz; @@ -809,7 +885,31 @@ void singleParam(int parameter) {} "Nullable<[[Nullable] -> SuperBuilderTargetBuilder] -> SuperBuilderTarget>", false, distinctElementsRatio(0.4), - distinctElementsRatio(0.4))); + distinctElementsRatio(0.4)), + arguments( + new TypeHolder() {}.annotatedType(), + "Nullable<([Boolean] -> A1 | ([Boolean] -> B1 | [Boolean] -> B2) | ([Boolean] -> C1 |" + + " [Integer] -> C2))>", + true, + contains( + null, + new Sealed.A.A1(false), + new Sealed.B.B1(false), + new Sealed.B.B2(false), + new Sealed.C.C1(false), + new Sealed.C.C2(0)), + contains( + null, + new Sealed.A.A1(false), + new Sealed.B.B1(false), + new Sealed.B.B2(false), + new Sealed.C.C1(false), + new Sealed.C.C2(0), + new Sealed.A.A1(true), + new Sealed.B.B1(true), + new Sealed.B.B2(true), + new Sealed.C.C1(true), + new Sealed.C.C2(1)))); } public static Stream protoStressTestCases() { From a2083fb508001ab759fb3404bac4e250ad27a87e Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 14 Jan 2025 17:48:25 +0100 Subject: [PATCH 10/20] Fix invalid javadoc comments --- .../jazzer/api/Autofuzz.java | 63 ++++++++++--------- .../jazzer/api/MethodHook.java | 2 + .../mutation/annotation/UrlSegment.java | 4 +- .../utils/ValidateContainerDimensions.java | 2 +- .../jazzer/mutation/utils/ValidateMinMax.java | 2 +- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java index ae525c6c5..ec30b7592 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java +++ b/src/main/java/com/code_intelligence/jazzer/api/Autofuzz.java @@ -146,15 +146,15 @@ private Autofuzz() {} *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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::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 R autofuzz(FuzzedDataProvider data, Function1 func) { @@ -176,15 +176,15 @@ public static R autofuzz(FuzzedDataProvider data, Function1 func) *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 R autofuzz(FuzzedDataProvider data, Function2 func) { @@ -206,15 +206,15 @@ public static R autofuzz(FuzzedDataProvider data, Function2Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 R autofuzz(FuzzedDataProvider data, Function3 func) { @@ -236,15 +236,15 @@ public static R autofuzz(FuzzedDataProvider data, Function3Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 R autofuzz( @@ -267,15 +267,15 @@ public static R autofuzz( *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 R autofuzz( @@ -298,13 +298,13 @@ public static R autofuzz( *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 void autofuzz(FuzzedDataProvider data, Consumer1 func) { try { @@ -323,13 +323,13 @@ public static void autofuzz(FuzzedDataProvider data, Consumer1 func) { *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 void autofuzz(FuzzedDataProvider data, Consumer2 func) { try { @@ -348,13 +348,13 @@ public static void autofuzz(FuzzedDataProvider data, Consumer2 *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 void autofuzz(FuzzedDataProvider data, Consumer3 func) { try { @@ -373,13 +373,13 @@ public static void autofuzz(FuzzedDataProvider data, Consumer3Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 void autofuzz( FuzzedDataProvider data, Consumer4 func) { @@ -399,13 +399,13 @@ public static void autofuzz( *

Note: This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 void autofuzz( FuzzedDataProvider data, Consumer5 func) { @@ -425,6 +425,9 @@ public static void autofuzz( *

Note: This function is inherently heuristic and may fail to return meaningful values * for a variety of reasons. * + *

May throw (unchecked) any {@link Throwable} thrown by {@code func} or an {@link + * AutofuzzConstructionException} if autofuzz failed to construct the arguments for the call. + * * @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 diff --git a/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java index 0df33c20d..bb1a8a405 100644 --- a/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java +++ b/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java @@ -119,6 +119,8 @@ * 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) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/UrlSegment.java b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/UrlSegment.java index 6a938dd61..3cc8f04fd 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/annotation/UrlSegment.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/annotation/UrlSegment.java @@ -27,8 +27,8 @@ /** * An annotation that applies to {@link String} and limits the character set of the * annotated type to valid URL segment characters, as described in RFC 3986, appendix A.
Can be combined with - * {@link WithUtf8Length} to limit the length of the generated string. + * href="https://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A.
+ * Can be combined with {@link WithUtf8Length} to limit the length of the generated string. */ @Target(TYPE_USE) @Retention(RUNTIME) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateContainerDimensions.java b/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateContainerDimensions.java index 3b5ebe5c5..a67c44e5c 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateContainerDimensions.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateContainerDimensions.java @@ -25,7 +25,7 @@ /** * Meta-annotation intended to be used internally by Jazzer for container annotations with min and * max fields. Annotations annotated with @ValidateContainerDimensions will be validated to ensure - * that min and max are both >= 0, and that min <= max. + * that min and max are both {@code >= 0}, and that {@code min <= max}. */ @Target(ANNOTATION_TYPE) @Retention(RUNTIME) diff --git a/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateMinMax.java b/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateMinMax.java index 3b3e4216c..69d4528d4 100644 --- a/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateMinMax.java +++ b/src/main/java/com/code_intelligence/jazzer/mutation/utils/ValidateMinMax.java @@ -24,7 +24,7 @@ /** * Meta-annotation intended to be used internally by Jazzer for annotations that have min and max - * fields. For all such annotations, Jazzer will assert that min <= max. + * fields. For all such annotations, Jazzer will assert that {@code min <= max}. */ @Target(ANNOTATION_TYPE) @Retention(RUNTIME) From 7f9015b9ce16f23669d3aca4c136cbe5452d5b56 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 14 Jan 2025 17:49:05 +0100 Subject: [PATCH 11/20] Include mutator annotations in jazzer-api Also uses `exports` rather than `runtime_deps` to mark those dependencies that users of the Maven artifact should be allowed to compile against (rather than just being available on the runtime classpath). This is necessary after https://github.com/bazel-contrib/rules_jvm_external/commit/7b0abdc591b9b0b0dbdfd62bf9d98928b0efe6a2. --- deploy/BUILD.bazel | 17 ++++++++++++----- deploy/jazzer-api_artifact_test.sh | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/deploy/BUILD.bazel b/deploy/BUILD.bazel index 1567d999f..a89b914f1 100644 --- a/deploy/BUILD.bazel +++ b/deploy/BUILD.bazel @@ -40,7 +40,11 @@ java_export( pom_template = "//deploy:jazzer-api.pom", toolchains = [":jazzer_version"], visibility = ["//visibility:public"], - runtime_deps = ["//src/main/java/com/code_intelligence/jazzer/api"], + exports = [ + "//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/annotation/proto", + ], ) java_export( @@ -95,13 +99,16 @@ java_export( pom_template = "jazzer-junit.pom", toolchains = [":jazzer_version"], visibility = ["//visibility:public"], - runtime_deps = [ - # These deps' only effect is to include a dependency on the 'jazzer' and 'jazzer-api' Maven artifacts in the - # POM. - "//deploy:jazzer", + exports = [ + # Maven users should not need to depend on jazzer-api directly if they already directly depend on jazzer-junit, + # both for convenience and backwards compatibility. "//deploy:jazzer-api", "//src/main/java/com/code_intelligence/jazzer/junit", ], + runtime_deps = [ + # This dep's only effect is to include a dependency on the 'jazzer' Maven artifacts in the POM. + "//deploy:jazzer", + ], ) sh_test( diff --git a/deploy/jazzer-api_artifact_test.sh b/deploy/jazzer-api_artifact_test.sh index c1aabbc88..d1a60e608 100755 --- a/deploy/jazzer-api_artifact_test.sh +++ b/deploy/jazzer-api_artifact_test.sh @@ -27,6 +27,9 @@ JAR="$2/bin/jar" -e '^com/code_intelligence/$' \ -e '^com/code_intelligence/jazzer/$' \ -e '^com/code_intelligence/jazzer/api/' \ + -e '^com/code_intelligence/jazzer/mutation/$' \ + -e '^com/code_intelligence/jazzer/mutation/annotation/' \ + -e '^com/code_intelligence/jazzer/mutation/utils/' \ -e '^jaz/' \ -e '^META-INF/$' \ -e '^META-INF/MANIFEST.MF$' From bb9bc9e177cb727561d8c16bc21ade499e0af5e9 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 17 Jan 2025 23:05:03 +0100 Subject: [PATCH 12/20] Update rules_jvm_external to fix POM Also update the direct deps that are updated as transitive deps of r_j_e to resolve warnings. --- MODULE.bazel | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 2bd6ee89e..d3581686b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -6,7 +6,7 @@ module(name = "jazzer") # Kept up-to-date by Renovate ################################################################################ -bazel_dep(name = "abseil-cpp", version = "20230802.0.bcr.1") +bazel_dep(name = "abseil-cpp", version = "20230802.1") bazel_dep(name = "apple_support", version = "1.11.1") bazel_dep(name = "bazel_jar_jar", version = "0.1.0") bazel_dep(name = "bazel_skylib", version = "1.7.1") @@ -18,11 +18,20 @@ bazel_dep(name = "protobuf") bazel_dep(name = "rules_android", version = "0.1.1") bazel_dep(name = "rules_android_ndk", version = "0.1.2") bazel_dep(name = "rules_foreign_cc", version = "0.11.1") -bazel_dep(name = "rules_java", version = "7.7.0") +bazel_dep(name = "rules_java", version = "7.12.2") bazel_dep(name = "rules_jni", version = "0.9.1") -bazel_dep(name = "rules_jvm_external", version = "6.2") -bazel_dep(name = "rules_kotlin", version = "1.9.5") -bazel_dep(name = "rules_license", version = "0.0.8") +bazel_dep(name = "rules_jvm_external") + +# TODO: Remove after the next release. +archive_override( + module_name = "rules_jvm_external", + integrity = "sha256-7AerLOLhQ+oIDH2id7OE8WJmbH01MqBWV4CbqJ6Nh68=", + strip_prefix = "rules_jvm_external-a1d4e4f4267c1797b686719aa385e707b732c541", + urls = ["https://github.com/bazelbuild/rules_jvm_external/archive/a1d4e4f4267c1797b686719aa385e707b732c541.tar.gz"], +) + +bazel_dep(name = "rules_kotlin", version = "1.9.6") +bazel_dep(name = "rules_license", version = "1.0.0") bazel_dep(name = "rules_pkg", version = "0.9.1") bazel_dep(name = "toolchains_llvm", version = "0.10.3") From 00c280112593c3567c31904bdd1a3c676ae0be1e Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Fri, 17 Jan 2025 23:05:03 +0100 Subject: [PATCH 13/20] Fix format script to actually format `.kt` files --- format.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/format.sh b/format.sh index 81d27eb5d..ceeca2ef3 100755 --- a/format.sh +++ b/format.sh @@ -35,9 +35,9 @@ if [[ "${CI:-0}" == 0 ]]; then # Check which ktlint_tests failed and run the corresponding fix targets. This is much faster than # running all ktlint_fix targets when e.g. only a few or no .kt files changed. # shellcheck disable=SC2046 - TARGETS_TO_RUN=$(bazel test --config=quiet $(bazel query --config=quiet 'kind(ktlint_test, //...)') | { grep FAILED || true; } | cut -f1 -d' ' | sed -e 's/:ktlint_test/:ktlint_fix/g') + TARGETS_TO_RUN=$(bazel test --config=quiet $(bazel query --config=quiet 'kind(ktlint_test, //...)') | { grep FAILED || true; } | cut -f1 -d' ' | sed -e 's/:ktlint_test/:ktlint_fix/g' || true) if [[ -n "${TARGETS_TO_RUN}" ]]; then - echo "$TARGETS_TO_RUN" | xargs -n 1 bazel run --config=quiet + echo "$TARGETS_TO_RUN" | xargs -I '{}' -n 1 bazel run --config=quiet {} -- --format fi # BUILD files From 711450aaef0aad4af3a49193d35eea6a5fd646e0 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Sat, 18 Jan 2025 11:49:39 +0100 Subject: [PATCH 14/20] Run `./format.sh` --- .../java/com/example/ExampleKotlinFuzzer.kt | 7 +- .../ExampleKotlinValueProfileFuzzer.kt | 5 +- .../src/main/java/com/example/KlaxonFuzzer.kt | 1 - .../jazzer/sanitizers/Deserialization.kt | 19 +- .../sanitizers/ExpressionLanguageInjection.kt | 9 +- .../jazzer/sanitizers/LdapInjection.kt | 9 +- .../jazzer/sanitizers/NamingContextLookup.kt | 8 +- .../jazzer/sanitizers/OsCommandInjection.kt | 12 +- .../jazzer/sanitizers/ReflectiveCall.kt | 68 ++++- .../jazzer/sanitizers/RegexInjection.kt | 25 +- .../jazzer/sanitizers/Utils.kt | 16 +- .../jazzer/sanitizers/XPathInjection.kt | 12 +- .../instrumentor/DirectByteBufferStrategy.kt | 7 +- .../code_intelligence/jazzer/agent/Agent.kt | 166 ++++++------ .../jazzer/agent/CoverageIdStrategy.kt | 69 +++-- .../jazzer/agent/RuntimeInstrumentor.kt | 103 ++++--- .../jazzer/driver/ExceptionUtils.kt | 116 ++++---- .../jazzer/instrumentor/ClassInstrumentor.kt | 38 +-- .../jazzer/instrumentor/CoverageRecorder.kt | 126 +++++---- .../jazzer/instrumentor/DescriptorUtils.kt | 45 ++-- .../instrumentor/DeterministicRandom.kt | 22 +- .../instrumentor/EdgeCoverageInstrumentor.kt | 68 +++-- .../jazzer/instrumentor/Hook.kt | 82 +++--- .../jazzer/instrumentor/HookInstrumentor.kt | 55 ++-- .../jazzer/instrumentor/HookMethodVisitor.kt | 156 ++++++----- .../jazzer/instrumentor/Hooks.kt | 87 +++--- .../jazzer/instrumentor/Instrumentor.kt | 15 +- .../instrumentor/TraceDataFlowInstrumentor.kt | 251 ++++++++++-------- .../jazzer/utils/ClassNameGlobber.kt | 87 +++--- .../jazzer/utils/ManifestUtils.kt | 15 +- .../jazzer/utils/SimpleGlobMatcher.kt | 4 +- .../code_intelligence/jazzer/utils/Utils.kt | 42 +-- .../instrumentor/AfterHooksPatchTest.kt | 25 +- .../instrumentor/BeforeHooksPatchTest.kt | 25 +- .../CoverageInstrumentationTest.kt | 75 +++--- .../instrumentor/DescriptorUtilsTest.kt | 69 +++-- .../jazzer/instrumentor/PatchTestUtils.kt | 29 +- .../instrumentor/ReplaceHooksPatchTest.kt | 25 +- .../TraceDataFlowInstrumentationTest.kt | 23 +- .../com/example/KotlinStringCompareFuzzer.kt | 3 +- .../src/test/java/com/example/KotlinVararg.kt | 4 +- 41 files changed, 1169 insertions(+), 854 deletions(-) diff --git a/examples/src/main/java/com/example/ExampleKotlinFuzzer.kt b/examples/src/main/java/com/example/ExampleKotlinFuzzer.kt index 556bbb38e..624df04af 100644 --- a/examples/src/main/java/com/example/ExampleKotlinFuzzer.kt +++ b/examples/src/main/java/com/example/ExampleKotlinFuzzer.kt @@ -20,13 +20,16 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium object ExampleKotlinFuzzer { - @JvmStatic fun fuzzerTestOneInput(data: FuzzedDataProvider) { exploreMe(data.consumeString(8), data.consumeInt(), data.consumeRemainingAsString()) } - private fun exploreMe(prefix: String, n: Int, suffix: String) { + private fun exploreMe( + prefix: String, + n: Int, + suffix: String, + ) { if (prefix.findAnyOf(arrayListOf("Fuzz", "Test")) != null) { if (n >= 2000000) { if (suffix.startsWith("@")) { diff --git a/examples/src/main/java/com/example/ExampleKotlinValueProfileFuzzer.kt b/examples/src/main/java/com/example/ExampleKotlinValueProfileFuzzer.kt index 536c8096e..f6d337784 100644 --- a/examples/src/main/java/com/example/ExampleKotlinValueProfileFuzzer.kt +++ b/examples/src/main/java/com/example/ExampleKotlinValueProfileFuzzer.kt @@ -20,7 +20,6 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium object ExampleKotlinValueProfileFuzzer { - @JvmStatic fun fuzzerTestOneInput(data: FuzzedDataProvider) { if (data.consumeInt().compareTo(0x11223344) != 0) { @@ -33,7 +32,5 @@ object ExampleKotlinValueProfileFuzzer { } } - private fun encrypt(n: Long): Long { - return n.xor(0x1122334455667788) - } + private fun encrypt(n: Long): Long = n.xor(0x1122334455667788) } diff --git a/examples/src/main/java/com/example/KlaxonFuzzer.kt b/examples/src/main/java/com/example/KlaxonFuzzer.kt index eb4633171..5f15be3dc 100644 --- a/examples/src/main/java/com/example/KlaxonFuzzer.kt +++ b/examples/src/main/java/com/example/KlaxonFuzzer.kt @@ -22,7 +22,6 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider // Reproduces https://github.com/cbeust/klaxon/pull/330 object KlaxonFuzzer { - @JvmStatic fun fuzzerTestOneInput(data: FuzzedDataProvider) { try { diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt index bd35ba3ef..d864a164d 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt @@ -33,7 +33,6 @@ import java.util.WeakHashMap */ @Suppress("unused_parameter", "unused") object Deserialization { - private val OBJECT_INPUT_STREAM_HEADER = ObjectStreamConstants.STREAM_MAGIC.toBytes() + ObjectStreamConstants.STREAM_VERSION.toBytes() @@ -88,13 +87,19 @@ object Deserialization { targetMethodDescriptor = "(Ljava/io/InputStream;)V", ) @JvmStatic - fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array, hookId: Int) { + fun objectInputStreamInitBeforeHook( + method: MethodHandle?, + alwaysNull: Any?, + args: Array, + hookId: Int, + ) { val originalInputStream = args[0] as? InputStream ?: return - val fixedInputStream = if (originalInputStream.markSupported()) { - originalInputStream - } else { - BufferedInputStream(originalInputStream) - } + val fixedInputStream = + if (originalInputStream.markSupported()) { + originalInputStream + } else { + BufferedInputStream(originalInputStream) + } args[0] = fixedInputStream guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt index 3de0dc299..0dbb16794 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt @@ -28,7 +28,6 @@ import java.lang.invoke.MethodHandle */ @Suppress("unused_parameter", "unused") object ExpressionLanguageInjection { - /** * Try to call the default constructor of the honeypot class. */ @@ -71,7 +70,9 @@ object ExpressionLanguageInjection { hookId: Int, ) { // The overloads taking a second string argument have either three or four arguments - if (arguments.size < 3) { return } + if (arguments.size < 3) { + return + } val expression = arguments[1] as? String ?: return Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) } @@ -95,7 +96,9 @@ object ExpressionLanguageInjection { arguments: Array, hookId: Int, ) { - if (arguments.size != 1) { return } + if (arguments.size != 1) { + return + } val message = arguments[0] as String Jazzer.guideTowardsContainment(message, EXPRESSION_LANGUAGE_ATTACK, hookId) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt index 54db3d868..087c93014 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -44,7 +44,6 @@ import javax.naming.directory.InvalidSearchFilterException */ @Suppress("unused_parameter", "unused") object LdapInjection { - // Characters to escape in DNs private const val NAME_CHARACTERS = "\\+<>,;\"=" @@ -67,7 +66,6 @@ object LdapInjection { targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;[Ljava/lang/Sting;)Ljavax/naming/NamingEnumeration;", additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], ), - // Object search, possible DN and search filter injection MethodHook( type = HookType.REPLACE, @@ -92,7 +90,12 @@ object LdapInjection { ), ) @JvmStatic - fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array, hookId: Int): Any? { + fun searchLdapContext( + method: MethodHandle, + thisObject: Any?, + args: Array, + hookId: Int, + ): Any? { try { return method.invokeWithArguments(thisObject, *args).also { (args[0] as? String)?.let { name -> diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt index 586acb426..b297607ab 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt @@ -26,7 +26,6 @@ import javax.naming.CommunicationException @Suppress("unused") object NamingContextLookup { - // The particular URL g.co is used here since it is: // - short, which makes it easier for the fuzzer to incorporate into the input; // - valid, which means that a `lookup` call on it could actually result in RCE; @@ -50,7 +49,12 @@ object NamingContextLookup { ), ) @JvmStatic - fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array, hookId: Int): Any { + fun lookupHook( + method: MethodHandle?, + thisObject: Any?, + args: Array, + hookId: Int, + ): Any { val name = args[0] as? String ?: throw CommunicationException() if (name.startsWith(RMI_MARKER) || name.startsWith(LDAP_MARKER)) { Jazzer.reportFindingFromHook( diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt index 5ecb08f0f..1c798fbbc 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt @@ -33,7 +33,6 @@ import java.lang.invoke.MethodHandle */ @Suppress("unused_parameter", "unused") object OsCommandInjection { - // Short and probably non-existing command name private const val COMMAND = "jazze" @@ -44,8 +43,15 @@ object OsCommandInjection { additionalClassesToHook = ["java.lang.ProcessBuilder"], ) @JvmStatic - fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array, hookId: Int) { - if (args.isEmpty()) { return } + fun processImplStartHook( + method: MethodHandle?, + alwaysNull: Any?, + args: Array, + hookId: Int, + ) { + if (args.isEmpty()) { + return + } // Calling ProcessBuilder already checks if command array is empty @Suppress("UNCHECKED_CAST") (args[0] as? Array)?.first().let { cmd -> diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt index 2c1db911c..91a5fbb01 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt @@ -30,25 +30,64 @@ import java.lang.invoke.MethodHandle */ @Suppress("unused_parameter", "unused") object ReflectiveCall { - @MethodHooks( - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"), - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;Z)Ljava/lang/Class;"), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.Class", + targetMethod = "forName", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.Class", + targetMethod = "forName", + targetMethodDescriptor = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ClassLoader", + targetMethod = "loadClass", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ClassLoader", + targetMethod = "loadClass", + targetMethodDescriptor = "(Ljava/lang/String;Z)Ljava/lang/Class;", + ), ) @JvmStatic - fun loadClassHook(method: MethodHandle?, alwaysNull: Any?, args: Array, hookId: Int) { + fun loadClassHook( + method: MethodHandle?, + alwaysNull: Any?, + args: Array, + hookId: Int, + ) { val className = args[0] as? String ?: return Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) } @MethodHooks( - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), - MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.Class", + targetMethod = "forName", + targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ClassLoader", + targetMethod = "loadClass", + targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;", + ), ) @JvmStatic - fun loadClassWithModuleHook(method: MethodHandle?, alwaysNull: Any?, args: Array, hookId: Int) { + fun loadClassWithModuleHook( + method: MethodHandle?, + alwaysNull: Any?, + args: Array, + hookId: Int, + ) { val className = args[1] as? String ?: return Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) } @@ -62,8 +101,15 @@ object ReflectiveCall { MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "findLibrary"), ) @JvmStatic - fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array, hookId: Int) { - if (args.isEmpty()) { return } + fun loadLibraryHook( + method: MethodHandle?, + alwaysNull: Any?, + args: Array, + hookId: Int, + ) { + if (args.isEmpty()) { + return + } val libraryName = args[0] as? String ?: return if (libraryName == HONEYPOT_LIBRARY_NAME) { Jazzer.reportFindingFromHook( diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt index d2faa11ca..19a4f5d21 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt @@ -51,7 +51,12 @@ object RegexInjection { targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", ) @JvmStatic - fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array, hookId: Int): Any? { + fun compileWithFlagsHook( + method: MethodHandle, + alwaysNull: Any?, + args: Array, + hookId: Int, + ): Any? { val pattern = args[0] as String? val hasCanonEqFlag = ((args[1] as Int) and Pattern.CANON_EQ) != 0 return hookInternal(method, pattern, hasCanonEqFlag, hookId, *args) @@ -72,9 +77,12 @@ object RegexInjection { ), ) @JvmStatic - fun patternHook(method: MethodHandle, alwaysNull: Any?, args: Array, hookId: Int): Any? { - return hookInternal(method, args[0] as String?, false, hookId, *args) - } + fun patternHook( + method: MethodHandle, + alwaysNull: Any?, + args: Array, + hookId: Int, + ): Any? = hookInternal(method, args[0] as String?, false, hookId, *args) @MethodHooks( MethodHook( @@ -109,9 +117,12 @@ object RegexInjection { ), ) @JvmStatic - fun stringHook(method: MethodHandle, thisObject: Any?, args: Array, hookId: Int): Any? { - return hookInternal(method, args[0] as String?, false, hookId, thisObject, *args) - } + fun stringHook( + method: MethodHandle, + thisObject: Any?, + args: Array, + hookId: Int, + ): Any? = hookInternal(method, args[0] as String?, false, hookId, thisObject, *args) private fun hookInternal( method: MethodHandle, diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt index 9e1beef51..592b3120c 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt @@ -25,12 +25,11 @@ import java.io.InputStream const val HONEYPOT_CLASS_NAME = "jaz.Zer" const val HONEYPOT_LIBRARY_NAME = "jazzer_honeypot" -internal fun Short.toBytes(): ByteArray { - return byteArrayOf( +internal fun Short.toBytes(): ByteArray = + byteArrayOf( ((toInt() shr 8) and 0xFF).toByte(), (toInt() and 0xFF).toByte(), ) -} // Runtime is only O(size * needle.size), only use for small arrays. internal fun ByteArray.indexOf(needle: ByteArray): Int { @@ -45,8 +44,15 @@ internal fun ByteArray.indexOf(needle: ByteArray): Int { return -1 } -internal fun guideMarkableInputStreamTowardsEquality(stream: InputStream, target: ByteArray, id: Int) { - fun readBytes(stream: InputStream, size: Int): ByteArray { +internal fun guideMarkableInputStreamTowardsEquality( + stream: InputStream, + target: ByteArray, + id: Int, +) { + fun readBytes( + stream: InputStream, + size: Int, + ): ByteArray { val current = ByteArray(size) var n = 0 while (n < size) { diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt index cc5095cc3..af6308c75 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt @@ -36,7 +36,6 @@ import javax.xml.xpath.XPathExpressionException */ @Suppress("unused_parameter", "unused") object XPathInjection { - // Characters that should be escaped in user input. // https://owasp.org/www-community/attacks/XPATH_Injection private const val CHARACTERS_TO_ESCAPE = "'\"" @@ -49,7 +48,12 @@ object XPathInjection { MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "evaluateExpression"), ) @JvmStatic - fun checkXpathExecute(method: MethodHandle, thisObject: Any?, arguments: Array, hookId: Int): Any { + fun checkXpathExecute( + method: MethodHandle, + thisObject: Any?, + arguments: Array, + hookId: Int, + ): Any { if (arguments.isNotEmpty() && arguments[0] is String) { val query = arguments[0] as String Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId) @@ -67,8 +71,8 @@ object XPathInjection { Jazzer.reportFindingFromHook( FuzzerSecurityIssueHigh( """ - XPath Injection - Injected query: ${arguments[0]} + XPath Injection + Injected query: ${arguments[0]} """.trimIndent(), exception, ), diff --git a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt index fdb79d24d..9ef3f0abd 100644 --- a/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt +++ b/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt @@ -20,7 +20,6 @@ import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes object DirectByteBufferStrategy : EdgeCoverageStrategy { - override fun instrumentControlFlowEdge( mv: MethodVisitor, edgeId: Int, @@ -66,7 +65,11 @@ object DirectByteBufferStrategy : EdgeCoverageStrategy { override val localVariableType get() = "java/nio/ByteBuffer" - override fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) { + override fun loadLocalVariable( + mv: MethodVisitor, + variable: Int, + coverageMapInternalClassName: String, + ) { mv.apply { visitFieldInsn( Opcodes.GETSTATIC, diff --git a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt index 2ed7e814b..2463ff1db 100644 --- a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt +++ b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt @@ -63,55 +63,61 @@ fun installInternal( 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 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 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 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 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() } - 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 @@ -121,39 +127,39 @@ fun installInternal( Hooks.appendHooksToBootstrapClassLoaderSearch(instrumentation, customHookNames.toSet()) val (includedHooks, customHooks) = Hooks.loadHooks(additionalClassesExcludes, includedHookNames.toSet(), customHookNames.toSet()) - val runtimeInstrumentor = RuntimeInstrumentor( - instrumentation, - classNameGlobber, - customHookClassNameGlobber, - instrumentOnly.isNotEmpty(), - instrumentationTypes, - includedHooks.hooks, - customHooks.hooks, - conditionalHooks, - customHooks.additionalHookClassNameGlobber, - coverageIdSynchronizer, - dumpClassesDirPath, - ) + val runtimeInstrumentor = + RuntimeInstrumentor( + instrumentation, + classNameGlobber, + customHookClassNameGlobber, + instrumentOnly.isNotEmpty(), + instrumentationTypes, + includedHooks.hooks, + customHooks.hooks, + conditionalHooks, + 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 { - // Always exclude internal Jazzer classes from retransformation, as even attempting to - // retransform those caused broken class definitions in older JVM versions. This points - // to a JDK bug that was not backported. - !it.name.startsWith("com.code_intelligence.jazzer.") && - ( - classNameGlobber.includes(it.name) || - customHookClassNameGlobber.includes(it.name) || - customHooks.additionalHookClassNameGlobber.includes(it.name) + val classesToRetransform = + instrumentation.allLoadedClasses + .filter { + // Always exclude internal Jazzer classes from retransformation, as even attempting to + // retransform those caused broken class definitions in older JVM versions. This points + // to a JDK bug that was not backported. + !it.name.startsWith("com.code_intelligence.jazzer.") && + ( + classNameGlobber.includes(it.name) || + customHookClassNameGlobber.includes(it.name) || + customHooks.additionalHookClassNameGlobber.includes(it.name) ) - } - .filter { - instrumentation.isModifiableClass(it) - } - .toTypedArray() + }.filter { + instrumentation.isModifiableClass(it) + }.toTypedArray() instrumentation.addTransformer(runtimeInstrumentor, true) @@ -164,7 +170,10 @@ fun installInternal( } } -private fun retransformClassesWithRetry(instrumentation: Instrumentation, classesToRetransform: Array>) { +private fun retransformClassesWithRetry( + instrumentation: Instrumentation, + classesToRetransform: Array>, +) { try { instrumentation.retransformClasses(*classesToRetransform) } catch (e: Throwable) { @@ -174,11 +183,16 @@ private fun retransformClassesWithRetry(instrumentation: Instrumentation, classe // 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)) + retransformClassesWithRetry( + instrumentation, + classesToRetransform.copyOfRange(classesToRetransform.size / 2, classesToRetransform.size), + ) } } } -private fun findManifestCustomHookNames() = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES) - .flatMap { it.split(':') } - .filter { it.isNotBlank() } +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/CoverageIdStrategy.kt b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt index cf5c8291e..bd7b50538 100644 --- a/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt +++ b/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt @@ -27,8 +27,9 @@ 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) +class CoverageIdException( + cause: Throwable? = null, +) : RuntimeException("Failed to synchronize coverage IDs", cause) /** * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation. @@ -38,13 +39,15 @@ class CoverageIdException(cause: Throwable? = null) : * 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) + fun withIdForClass( + className: String, + block: (Int) -> Int, + ) } /** @@ -60,7 +63,10 @@ class MemSyncCoverageIdStrategy : CoverageIdStrategy { private var nextEdgeId = 0 @Synchronized - override fun withIdForClass(className: String, block: (Int) -> Int) { + override fun withIdForClass( + className: String, + block: (Int) -> Int, + ) { nextEdgeId += block(nextEdgeId) } } @@ -71,7 +77,9 @@ class MemSyncCoverageIdStrategy : CoverageIdStrategy { * 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 { +class FileSyncCoverageIdStrategy( + private val idSyncFile: Path, +) : CoverageIdStrategy { private val uuid: UUID = UUID.randomUUID() private var idFileLock: FileLock? = null @@ -85,7 +93,10 @@ class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrat * is always committed back again to the sync file by [commitIdCount]. */ @Synchronized - override fun withIdForClass(className: String, block: (Int) -> Int) { + override fun withIdForClass( + className: String, + block: (Int) -> Int, + ) { var actualNumEdgeIds = 0 try { val firstId = obtainFirstId(className) @@ -108,32 +119,36 @@ class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrat 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, - ) + 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 // :: - 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 ':::', 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() + 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 ':::', 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) { diff --git a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt index 34ec4fd5b..3a44ed1e5 100644 --- a/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt @@ -52,7 +52,6 @@ class RuntimeInstrumentor( private val coverageIdSynchronizer: CoverageIdStrategy, private val dumpClassesDir: Path?, ) : ClassFileTransformer { - @kotlin.time.ExperimentalTime override fun transform( loader: ClassLoader?, @@ -67,10 +66,16 @@ class RuntimeInstrumentor( // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html return try { if (instrumentOnly && protectionDomain != null) { - var outputPathPrefix = protectionDomain.getCodeSource().getLocation().getFile().toString() + 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) + outputPathPrefix = + outputPathPrefix.substring(outputPathPrefix.lastIndexOf(File.separator) + 1, outputPathPrefix.length) } if (outputPathPrefix.endsWith(".jar")) { @@ -116,7 +121,12 @@ class RuntimeInstrumentor( } } - private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "", pathPrefix: String = "") { + 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() @@ -167,38 +177,44 @@ class RuntimeInstrumentor( } @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 - } + 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() - .acceptLibOrExtJars() - .ignoreClassVisibility() - .acceptClasses(className) - .scan() - .use { - it.getClassInfo(className)?.resource?.load() ?: run { - Log.warn("Failed to load bytecode of class $className") - return null + val classfileBuffer = + maybeClassfileBuffer ?: ClassGraph() + .enableSystemJarsAndModules() + .acceptLibOrExtJars() + .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 (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) { @@ -211,14 +227,19 @@ class RuntimeInstrumentor( return instrumentedBytecode } - private fun instrument(internalClassName: String, bytecode: ByteArray, fullInstrumentation: Boolean): ByteArray { - val classWithHooksEnabledField = if (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 - } + private fun instrument( + internalClassName: String, + bytecode: ByteArray, + fullInstrumentation: Boolean, + ): ByteArray { + val classWithHooksEnabledField = + if (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 diff --git a/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt index ba7ca359e..36c6cea3a 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt +++ b/src/main/java/com/code_intelligence/jazzer/driver/ExceptionUtils.kt @@ -29,15 +29,19 @@ 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 - } + 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 = +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). @@ -55,8 +59,7 @@ private fun hash(throwable: Throwable, passToRootCause: Boolean): ByteArray = it.className.startsWith("java.lang.reflect.") || it.className.startsWith("sun.reflect.") || it.className.startsWith("java.lang.invoke.") - } - .forEach { update(it.toString().toByteArray()) } + }.forEach { update(it.toString().toByteArray()) } if (throwable.suppressed.isNotEmpty()) { update("suppressed".toByteArray()) for (suppressed in throwable.suppressed) { @@ -87,36 +90,38 @@ fun computeDedupToken(throwable: Throwable): 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() - 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() +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() + 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)" + is OutOfMemoryError -> { + var securityIssueMessage = "Out of memory" + if (!IS_ANDROID) { + securityIssueMessage = "$securityIssueMessage (use '${getReproducingXmxArg()}' to reproduce)" + } + stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable)) } - stripOwnStackTrace(FuzzerSecurityIssueLow(securityIssueMessage, throwable)) - } - is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable)) - else -> throwable -}.also { dropInternalFrames(it) } + is VirtualMachineError -> stripOwnStackTrace(FuzzerSecurityIssueLow(throwable)) + else -> throwable + }.also { dropInternalFrames(it) } /** * Recursively strips all Jazzer-internal stack frames from the given [Throwable] and its causes. @@ -133,9 +138,10 @@ private fun dropInternalFrames(throwable: Throwable?) { * 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() -} +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 @@ -159,7 +165,11 @@ private fun getReproducingXssArg(): String? { private fun getNumericFinalFlagValue(arg: String): Long? { val argPattern = "$arg\\D*(\\d*)".toRegex() - return argPattern.find(javaFullFinalFlags ?: return null)?.groupValues?.get(1)?.toLongOrNull() + return argPattern + .find(javaFullFinalFlags ?: return null) + ?.groupValues + ?.get(1) + ?.toLongOrNull() } private val javaFullFinalFlags by lazy { @@ -170,12 +180,14 @@ 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() + 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") } @@ -188,15 +200,15 @@ fun dumpAllStackTraces() { for ((thread, stack) in Thread.getAllStackTraces()) { Log.println(thread.toString()) // Remove traces of this method and the methods it calls. - stack.asList() + stack + .asList() .asReversed() .takeWhile { !( it.className == "com.code_intelligence.jazzer.driver.ExceptionUtils" && it.methodName == "dumpAllStackTraces" - ) - } - .asReversed() + ) + }.asReversed() .forEach { frame -> Log.println("\tat $frame") } diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt index f024a7f63..51617ab0f 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt @@ -18,21 +18,23 @@ 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) { +fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int = + ((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, - ) + val edgeCoverageInstrumentor = + EdgeCoverageInstrumentor( + defaultEdgeCoverageStrategy, + defaultCoverageMap, + initialEdgeId, + ) instrumentedBytecode = edgeCoverageInstrumentor.instrument(internalClassName, instrumentedBytecode) return edgeCoverageInstrumentor.numEdges } @@ -42,12 +44,16 @@ class ClassInstrumentor(private val internalClassName: String, bytecode: ByteArr TraceDataFlowInstrumentor(instrumentations).instrument(internalClassName, instrumentedBytecode) } - fun hooks(hooks: Iterable, classWithHooksEnabledField: String?) { - instrumentedBytecode = HookInstrumentor( - hooks, - java6Mode = extractClassFileMajorVersion(instrumentedBytecode) < 51, - classWithHooksEnabledField = classWithHooksEnabledField, - ).instrument(internalClassName, instrumentedBytecode) + fun hooks( + hooks: Iterable, + classWithHooksEnabledField: String?, + ) { + instrumentedBytecode = + HookInstrumentor( + hooks, + java6Mode = extractClassFileMajorVersion(instrumentedBytecode) < 51, + classWithHooksEnabledField = classWithHooksEnabledField, + ).instrument(internalClassName, instrumentedBytecode) } companion object { diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt index 2e6299d82..0aab6ca85 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt @@ -44,16 +44,22 @@ object CoverageRecorder { private var startTimestamp: Instant? = null private val additionalCoverage = mutableSetOf() - fun recordInstrumentedClass(internalClassName: String, bytecode: ByteArray, firstId: Int, numIds: 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, - ) + instrumentedClassInfo[internalClassName] = + InstrumentedClassInfo( + CRC64.classId(bytecode), + firstId, + firstId + numIds, + bytecode, + ) } /** @@ -70,7 +76,10 @@ object CoverageRecorder { */ @JvmStatic @JvmOverloads - fun dumpCoverageReport(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) { + fun dumpCoverageReport( + dumpFileName: String, + coveredIds: IntArray = CoverageMap.getEverCoveredIds(), + ) { File(dumpFileName).bufferedWriter().use { writer -> writer.write(computeFileCoverage(coveredIds)) } @@ -87,32 +96,39 @@ object CoverageRecorder { 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() - } + } + + 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() + } } /** @@ -122,7 +138,10 @@ object CoverageRecorder { */ @JvmStatic @JvmOverloads - fun dumpJacocoCoverage(dumpFileName: String, coveredIds: IntArray = CoverageMap.getEverCoveredIds()) { + fun dumpJacocoCoverage( + dumpFileName: String, + coveredIds: IntArray = CoverageMap.getEverCoveredIds(), + ) { FileOutputStream(dumpFileName).use { outStream -> dumpJacocoCoverage(outStream, coveredIds) } @@ -132,7 +151,10 @@ object CoverageRecorder { * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream]. */ @JvmStatic - fun dumpJacocoCoverage(outStream: OutputStream, coveredIds: IntArray) { + fun dumpJacocoCoverage( + outStream: OutputStream, + coveredIds: IntArray, + ) { // Return if no class has been instrumented. val startTimestamp = startTimestamp ?: return @@ -173,12 +195,12 @@ object CoverageRecorder { } // 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() + (coveredIdsStart until coveredIdsEnd) + .asSequence() .map { val globalEdgeId = sortedCoveredIds[it] globalEdgeId - info.initialEdgeId - } - .forEach { classLocalEdgeId -> + }.forEach { classLocalEdgeId -> probes[classLocalEdgeId] = true } executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) @@ -189,8 +211,8 @@ object CoverageRecorder { /** * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics. */ - fun analyzeCoverage(coveredIds: Set): CoverageBuilder? { - return try { + fun analyzeCoverage(coveredIds: Set): CoverageBuilder? = + try { val coverage = CoverageBuilder() analyzeAllUncoveredClasses(coverage) val executionDataStore = analyzeJacocoCoverage(coveredIds) @@ -208,7 +230,6 @@ object CoverageRecorder { e.printStackTrace() null } - } /** * Traverses the entire classpath and analyzes all uncovered classes that match the include/exclude pattern. @@ -216,11 +237,12 @@ object CoverageRecorder { * those that were loaded while the fuzzer ran. */ private fun analyzeAllUncoveredClasses(coverage: CoverageBuilder): CoverageBuilder { - val coveredClassNames = instrumentedClassInfo - .keys - .asSequence() - .map { it.replace('/', '.') } - .toSet() + val coveredClassNames = + instrumentedClassInfo + .keys + .asSequence() + .map { it.replace('/', '.') } + .toSet() ClassGraph() .enableClassInfo() .ignoreClassVisibility() @@ -229,8 +251,8 @@ object CoverageRecorder { // from the Java standard library are never traversed. "com.code_intelligence.jazzer.*", "jaz", - ) - .scan().use { result -> + ).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() @@ -240,7 +262,11 @@ object CoverageRecorder { .filterNot { classInfo -> classInfo.name in coveredClassNames } .forEach { classInfo -> classInfo.resource.use { resource -> - EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze( + EdgeCoverageInstrumentor( + ClassInstrumentor.defaultEdgeCoverageStrategy, + ClassInstrumentor.defaultCoverageMap, + 0, + ).analyze( emptyExecutionDataStore, coverage, resource.load(), diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt index ab4df625c..7ec5431b2 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtils.kt @@ -25,40 +25,39 @@ 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<*>?) - } + 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") -} +internal fun isPrimitiveType(typeDescriptor: String): Boolean = 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 -} +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(";")) { +internal fun extractInternalClassName(typeDescriptor: String): String = + if (typeDescriptor.startsWith("L") && typeDescriptor.endsWith(";")) { typeDescriptor.substring(1, typeDescriptor.length - 1) } else { typeDescriptor } -} internal fun extractParameterTypeDescriptors(methodDescriptor: String): List { require(methodDescriptor.startsWith('(')) { "Method descriptor must start with '('" } diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt index 90a98dfb2..daef16429 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/DeterministicRandom.kt @@ -20,16 +20,20 @@ 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() +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) } - setSeed(contextHash) - } fun nextInt(bound: Int) = random.nextInt(bound) diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt index 6d9d1469b..eb7067947 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt @@ -41,7 +41,6 @@ import kotlin.math.max * 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 @@ -71,7 +70,11 @@ interface EdgeCoverageStrategy { * 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) + fun loadLocalVariable( + mv: MethodVisitor, + variable: Int, + coverageMapInternalClassName: String, + ) /** * The maximal number of stack elements used by [loadLocalVariable]. @@ -103,19 +106,28 @@ class EdgeCoverageInstrumentor( ), ) - override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + 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), - ) + 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) { + fun analyze( + executionData: ExecutionDataStore, + coverageVisitor: ICoverageVisitor, + bytecode: ByteArray, + internalClassName: String, + ) { Analyzer(executionData, coverageVisitor, edgeCoverageClassProbesAdapterFactory).run { analyzeClass(bytecode, internalClassName) } @@ -144,7 +156,10 @@ class EdgeCoverageInstrumentor( strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName) } - override fun visitMaxs(maxStack: Int, maxLocals: Int) { + 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) @@ -158,8 +173,10 @@ class EdgeCoverageInstrumentor( EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy) } - private inner class EdgeCoverageClassProbesAdapter(private val cpv: ClassProbesVisitor, trackFrames: Boolean) : - ClassProbesAdapter(cpv, trackFrames) { + private inner class EdgeCoverageClassProbesAdapter( + private val cpv: ClassProbesVisitor, + trackFrames: Boolean, + ) : ClassProbesAdapter(cpv, trackFrames) { override fun nextId(): Int = nextEdgeId() override fun visitEnd() { @@ -170,18 +187,27 @@ class EdgeCoverageInstrumentor( } } - 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 + private val edgeCoverageClassProbesAdapterFactory = + IClassProbesAdapterFactory { probesVisitor, trackFrames -> + EdgeCoverageClassProbesAdapter(probesVisitor, trackFrames) } - override fun addMembers(cv: ClassVisitor, probeCount: Int) {} - } + 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) { diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt index 1e1cb4877..55e4c6f26 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt @@ -38,21 +38,28 @@ class Hook private constructor( val hookMethodName: String, val hookMethodDescriptor: String, ) { - - override fun toString(): String { - return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook" - } + override fun toString(): String = + "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook" companion object { - fun createAndVerifyHook(hookMethod: Method, hookData: MethodHook, className: String): Hook { - return createHook(hookMethod, hookData, className).also { + fun createAndVerifyHook( + hookMethod: Method, + hookData: MethodHook, + className: String, + ): Hook = + 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) } + 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, @@ -70,7 +77,10 @@ class Hook private constructor( ) } - private fun verify(hookMethod: Method, potentialHook: Hook) { + 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" } @@ -78,39 +88,49 @@ class Hook private constructor( // 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.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[1] == Object::class.java || parameterTypes[1].name == potentialHook.targetClassName) { + "$potentialHook: second parameter must have type Object or ${potentialHook.targetClassName}" + } require(parameterTypes[2] == Array::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 == "") { - 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}" + HookType.BEFORE, HookType.AFTER -> + require(hookMethod.returnType == Void.TYPE) { + "$potentialHook: return type must be void" + } + HookType.REPLACE -> + if (potentialHook.targetReturnTypeDescriptor != null) { + if (potentialHook.targetMethodName == "") { + 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 diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt index 56a65ea0d..735deab5d 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt @@ -26,39 +26,42 @@ internal class HookInstrumentor( private val java6Mode: Boolean, private val classWithHooksEnabledField: String?, ) : Instrumentor { - private lateinit var random: DeterministicRandom - override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + 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?, - ): 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 + val interceptor = + object : ClassVisitor(Instrumentor.ASM_API_VERSION, writer) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): 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 index 5b85fb4cc..17c88011a 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt @@ -37,8 +37,8 @@ internal fun makeHookMethodVisitor( java6Mode: Boolean, random: DeterministicRandom, classWithHooksEnabledField: String?, -): MethodVisitor { - return HookMethodVisitor( +): MethodVisitor = + HookMethodVisitor( owner, access, name, @@ -49,7 +49,6 @@ internal fun makeHookMethodVisitor( random, classWithHooksEnabledField, ).lvs -} private class HookMethodVisitor( owner: String, @@ -62,50 +61,51 @@ private class HookMethodVisitor( 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 - }, -) { - + 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) { - // 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) + val lvs = + object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) { + override fun updateNewLocals(newLocals: Array) { + // 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}" + private val hooks = + hooks.groupBy { hook -> + var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}" + if (hook.targetMethodDescriptor != null) { + hookKey += "#${hook.targetMethodDescriptor}" + } + hookKey } - hookKey - } override fun visitMethodInsn( opcode: Int, @@ -233,13 +233,14 @@ private class HookMethodVisitor( } } 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 - } + 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 @@ -381,19 +382,28 @@ private class HookMethodVisitor( } } - private fun isMethodInvocationOp(opcode: Int) = opcode in listOf( - Opcodes.INVOKEVIRTUAL, - Opcodes.INVOKEINTERFACE, - Opcodes.INVOKESTATIC, - Opcodes.INVOKESPECIAL, - ) + 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 { - val result = HookType.values().flatMap { hookType -> - val withoutDescriptorKey = "$hookType#$owner#$name" - val withDescriptorKey = "$withoutDescriptorKey#$descriptor" - hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty() - }.sortedBy { it.hookType } + private fun findMatchingHooks( + owner: String, + name: String, + descriptor: String, + ): List { + 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 || @@ -457,7 +467,10 @@ private class HookMethodVisitor( // 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, localObjArr: Int) { + private fun loadMethodArguments( + paramDescriptors: List, + localObjArr: Int, + ) { // Loop over all arguments for ((argIdx, argDescriptor) in paramDescriptors.withIndex()) { // Push a reference to the object array on the stack @@ -494,17 +507,18 @@ private class HookMethodVisitor( // 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 - } + 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, diff --git a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt index 658bf233e..9d857524c 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt @@ -31,27 +31,31 @@ data class Hooks( val hookClasses: Set>, val additionalHookClassNameGlobber: ClassNameGlobber, ) { - companion object { - - fun appendHooksToBootstrapClassLoaderSearch(instrumentation: Instrumentation, hookClassNames: Set) { - 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() + fun appendHooksToBootstrapClassLoaderSearch( + instrumentation: Instrumentation, + hookClassNames: Set, + ) { + 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, vararg hookClassNames: Set): List { - return ClassGraph() + fun loadHooks( + excludeHookClassNames: List, + vararg hookClassNames: Set, + ): List = + ClassGraph() .enableClassInfo() .enableSystemJarsAndModules() .acceptLibOrExtJars() @@ -63,38 +67,40 @@ data class Hooks( val loader = HooksLoader(scanResult, excludeHookClassNames) hookClassNames.map(loader::load) } - } - - private class HooksLoader(private val scanResult: ScanResult, val excludeHookClassNames: List) { + private class HooksLoader( + private val scanResult: ScanResult, + val excludeHookClassNames: List, + ) { fun load(hookClassNames: Set): 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, - ) + val additionalHookClassNameGlobber = + ClassNameGlobber( + hooks.flatMap(Hook::additionalClassesToHook), + excludeHookClassNames, + ) return Hooks(hooks, hookClasses, additionalHookClassNameGlobber) } - private fun loadHooks(hookClassName: String): List>> { - return try { + private fun loadHooks(hookClassName: String): List>> = + 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 - } + 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 { val hooks = mutableListOf() @@ -111,23 +117,26 @@ data class Hooks( return hooks } - private fun verifyAndGetHooks(hookMethod: Method, hookData: MethodHook): List { - return lookupClassesToHook(hookData.targetClassName) + private fun verifyAndGetHooks( + hookMethod: Method, + hookData: MethodHook, + ): List = + lookupClassesToHook(hookData.targetClassName) .map { className -> Hook.createAndVerifyHook(hookMethod, hookData, className) } - } private fun lookupClassesToHook(annotationTargetClassName: String): List { // 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() - } + 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 index 2fb5ae55f..fa32f51c9 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt @@ -29,17 +29,18 @@ enum class InstrumentationType { } internal interface Instrumentor { - fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray + fun instrument( + internalClassName: String, + bytecode: ByteArray, + ): ByteArray - fun shouldInstrument(access: Int): Boolean { - return (access and Opcodes.ACC_ABSTRACT == 0) && + fun shouldInstrument(access: Int): Boolean = + (access and Opcodes.ACC_ABSTRACT == 0) && (access and Opcodes.ACC_NATIVE == 0) - } - fun shouldInstrument(method: MethodNode): Boolean { - return shouldInstrument(method.access) && + fun shouldInstrument(method: MethodNode): Boolean = + 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/TraceDataFlowInstrumentor.kt b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt index 686ca6dc0..e637e9515 100644 --- a/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt +++ b/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt @@ -36,10 +36,12 @@ internal class TraceDataFlowInstrumentor( private val types: Set, private val callbackInternalClassName: String = "com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks", ) : Instrumentor { - private lateinit var random: DeterministicRandom - override fun instrument(internalClassName: String, bytecode: ByteArray): ByteArray { + override fun instrument( + internalClassName: String, + bytecode: ByteArray, + ): ByteArray { val node = ClassNode() val reader = ClassReader(bytecode) reader.accept(node, 0) @@ -93,31 +95,33 @@ internal class TraceDataFlowInstrumentor( // 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 the switch is over String values, find out the actual values and not the hashes, and - // report them to libFuzzer in the switch's default case. - if (instrumentSwitchOverStrings(method, inst)) { - continue@loop - } - if (inst.keys.isEmpty() || (0 <= inst.keys.first() && inst.keys.last() < 256)) { - continue@loop + val caseValues = + when (inst) { + is LookupSwitchInsnNode -> { + // If the switch is over String values, find out the actual values and not the hashes, and + // report them to libFuzzer in the switch's default case. + if (instrumentSwitchOverStrings(method, inst)) { + continue@loop + } + if (inst.keys.isEmpty() || (0 <= inst.keys.first() && inst.keys.last() < 256)) { + continue@loop + } + inst.keys } - inst.keys - } - is TableSwitchInsnNode -> { - if (0 <= inst.min && inst.max < 256) { - continue@loop + 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() } - (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() + // Not reached. + else -> continue@loop + }.sortedBy { it.toUInt() }.map { it.toLong() }.toLongArray() method.instructions.insertBefore(inst, switchInstrumentation(caseValues)) } Opcodes.IDIV -> { @@ -244,69 +248,75 @@ internal class TraceDataFlowInstrumentor( 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 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 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 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 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 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()) { + private fun switchInstrumentation(caseValues: LongArray) = + InsnList().apply { + // duplicate {lookup,table}switch key for use as first function argument add(InsnNode(Opcodes.DUP)) - add(IntInsnNode(Opcodes.SIPUSH, 2 + i)) - add(LdcInsnNode(caseValue)) + 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)) } - 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 @@ -324,47 +334,54 @@ internal class TraceDataFlowInstrumentor( 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)) - } + 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 CONSTANT_INTEGER_PUSH_OPCODES = + listOf( + Opcodes.BIPUSH, + Opcodes.SIPUSH, + Opcodes.LDC, + Opcodes.ICONST_2, + Opcodes.ICONST_3, + Opcodes.ICONST_4, + Opcodes.ICONST_5, + ) - 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"), + 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/utils/ClassNameGlobber.kt b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt index 0a7346538..4f4846f62 100644 --- a/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt +++ b/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt @@ -16,53 +16,62 @@ package com.code_intelligence.jazzer.utils -private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf( - "**", // everything -) +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 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 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() +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, excludes: List) { +class ClassNameGlobber( + includes: List, + excludes: List, +) { // If no include globs are provided, start with all classes. - private val includeMatchers = includes.ifEmpty { BASE_INCLUDED_CLASS_NAME_GLOBS } - .map(::SimpleGlobMatcher) + 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) + 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) } - } + fun includes(className: String): Boolean = + includeMatchers.any { it.matches(className) } && excludeMatchers.none { it.matches(className) } } diff --git a/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt index c448c17ef..96bb0300d 100644 --- a/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt +++ b/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt @@ -19,18 +19,19 @@ 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 { 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() + return manifests + .asSequence() + .mapNotNull { url -> + url.openStream().use { inputStream -> + val manifest = Manifest(inputStream) + manifest.mainAttributes.getValue(attribute) + } + }.toList() } /** diff --git a/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt index 47a1e09a0..232b3a64b 100644 --- a/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt +++ b/src/main/java/com/code_intelligence/jazzer/utils/SimpleGlobMatcher.kt @@ -16,7 +16,9 @@ package com.code_intelligence.jazzer.utils -class SimpleGlobMatcher(val glob: String) { +class SimpleGlobMatcher( + val glob: String, +) { private enum class Type { // foo.bar (matches foo.bar only) FULL_MATCH, diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt index 92076813b..b9ec1a974 100644 --- a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt +++ b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt @@ -20,28 +20,30 @@ 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") + 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") } - 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 - } + get() = + parameterTypes.joinToString(separator = ",", prefix = "(", postfix = ")") { parameterType -> + parameterType.readableDescriptor + } diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt index d122e844a..4273e64b1 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -21,17 +21,16 @@ import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File -private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract { - return AfterHooksTarget() -} +private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract = AfterHooksTarget() private fun getNoHooksAfterHooksTargetInstance(): AfterHooksTargetContract { val originalBytecode = classToBytecode(AfterHooksTarget::class.java) // Let the bytecode pass through the hooking logic, but don't apply any hooks. - val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( - AfterHooksTarget::class.java.name.replace('.', '/'), - originalBytecode, - ) + val patchedBytecode = + HookInstrumentor(emptyList(), false, null).instrument( + AfterHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) val patchedClass = bytecodeToClass(AfterHooksTarget::class.java.name, patchedBytecode) return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract } @@ -39,11 +38,12 @@ private fun getNoHooksAfterHooksTargetInstance(): AfterHooksTargetContract { private fun getPatchedAfterHooksTargetInstance(classWithHooksEnabledField: Class<*>?): AfterHooksTargetContract { val originalBytecode = classToBytecode(AfterHooksTarget::class.java) val hooks = Hooks.loadHooks(emptyList(), setOf(AfterHooks::class.java.name)).first().hooks - val patchedBytecode = HookInstrumentor( - hooks, - false, - classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), - ).instrument(AfterHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(AfterHooksTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${AfterHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) @@ -53,7 +53,6 @@ private fun getPatchedAfterHooksTargetInstance(classWithHooksEnabledField: Class } class AfterHooksPatchTest { - @Test fun testOriginal() { assertSelfCheck(getOriginalAfterHooksTargetInstance(), false) diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt index 971455ffb..60d22aaf4 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -21,17 +21,16 @@ import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File -private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract { - return BeforeHooksTarget() -} +private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract = BeforeHooksTarget() private fun getNoHooksBeforeHooksTargetInstance(): BeforeHooksTargetContract { val originalBytecode = classToBytecode(BeforeHooksTarget::class.java) // Let the bytecode pass through the hooking logic, but don't apply any hooks. - val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( - BeforeHooksTarget::class.java.name.replace('.', '/'), - originalBytecode, - ) + val patchedBytecode = + HookInstrumentor(emptyList(), false, null).instrument( + BeforeHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) val patchedClass = bytecodeToClass(BeforeHooksTarget::class.java.name, patchedBytecode) return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract } @@ -39,11 +38,12 @@ private fun getNoHooksBeforeHooksTargetInstance(): BeforeHooksTargetContract { private fun getPatchedBeforeHooksTargetInstance(classWithHooksEnabledField: Class<*>?): BeforeHooksTargetContract { val originalBytecode = classToBytecode(BeforeHooksTarget::class.java) val hooks = Hooks.loadHooks(emptyList(), setOf(BeforeHooks::class.java.name)).first().hooks - val patchedBytecode = HookInstrumentor( - hooks, - false, - classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), - ).instrument(BeforeHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(BeforeHooksTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${BeforeHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) @@ -53,7 +53,6 @@ private fun getPatchedBeforeHooksTargetInstance(classWithHooksEnabledField: Clas } class BeforeHooksPatchTest { - @Test fun testOriginal() { assertSelfCheck(getOriginalBeforeHooksTargetInstance(), false) diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt index 23e2dd4da..a7254febf 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt @@ -41,17 +41,16 @@ private fun makeTestable(strategy: EdgeCoverageStrategy): EdgeCoverageStrategy = } } -private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { - return CoverageInstrumentationTarget() -} +private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract = CoverageInstrumentationTarget() private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { val originalBytecode = classToBytecode(CoverageInstrumentationTarget::class.java) - val patchedBytecode = EdgeCoverageInstrumentor( - makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), - MockCoverageMap::class.java, - 0, - ).instrument(CoverageInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0, + ).instrument(CoverageInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${CoverageInstrumentationTarget::class.java.simpleName}.class").writeBytes(originalBytecode) @@ -66,7 +65,6 @@ private fun assertControlFlow(expectedLocations: List) { @Suppress("unused") class CoverageInstrumentationTest { - private val constructorReturn = 0 private val mapConstructor = 1 @@ -105,26 +103,31 @@ class CoverageInstrumentationTest { val mapControlFlow = listOf(mapConstructor, addFor0, addFor1, addFor2, addFor3, addFor4, addFoobar) val ifControlFlow = listOf(ifTrueBranch, addBlock1, ifEnd) - val forFirstRunControlFlow = mutableListOf().apply { - add(outerForCondition) - repeat(5) { - addAll(listOf(innerForCondition, innerForBodyIfFalseBranch, innerForBodyPutInvocation)) - } - add(outerForIncrementCounter) - }.toList() - val forSecondRunControlFlow = mutableListOf().apply { - add(outerForCondition) - repeat(5) { - addAll(listOf(innerForCondition, innerForBodyIfTrueBranch, innerForBodyPutInvocation)) - } - add(outerForIncrementCounter) - }.toList() + val forFirstRunControlFlow = + mutableListOf() + .apply { + add(outerForCondition) + repeat(5) { + addAll(listOf(innerForCondition, innerForBodyIfFalseBranch, innerForBodyPutInvocation)) + } + add(outerForIncrementCounter) + }.toList() + val forSecondRunControlFlow = + mutableListOf() + .apply { + add(outerForCondition) + repeat(5) { + addAll(listOf(innerForCondition, innerForBodyIfTrueBranch, innerForBodyPutInvocation)) + } + add(outerForIncrementCounter) + }.toList() val forControlFlow = forFirstRunControlFlow + forSecondRunControlFlow - val fooCallControlFlow = listOf( - barAfterPutInvocation, - fooAfterBarInvocation, - afterFooInvocation, - ) + val fooCallControlFlow = + listOf( + barAfterPutInvocation, + fooAfterBarInvocation, + afterFooInvocation, + ) assertControlFlow( listOf(constructorReturn) + mapControlFlow + @@ -149,8 +152,9 @@ class CoverageInstrumentationTest { assertSelfCheck(target) assertEquals(1, MockCoverageMap.counters[takenOnceEdge]) // Verify that the counter increments, but is never zero. - val expectedCounter = (lastCounter + 1U).toUByte().takeUnless { it == 0.toUByte() } - ?: (lastCounter + 2U).toUByte() + val expectedCounter = + (lastCounter + 1U).toUByte().takeUnless { it == 0.toUByte() } + ?: (lastCounter + 2U).toUByte() lastCounter = expectedCounter val actualCounter = MockCoverageMap.counters[takenOnEveryRunEdge].toUByte() assertEquals(expectedCounter, actualCounter, "After $i runs:") @@ -160,11 +164,12 @@ class CoverageInstrumentationTest { @Test fun testSpecialCases() { val originalBytecode = classToBytecode(CoverageInstrumentationSpecialCasesTarget::class.java) - val patchedBytecode = EdgeCoverageInstrumentor( - makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), - MockCoverageMap::class.java, - 0, - ).instrument(CoverageInstrumentationSpecialCasesTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0, + ).instrument(CoverageInstrumentationSpecialCasesTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${CoverageInstrumentationSpecialCasesTarget::class.simpleName}.class").writeBytes(originalBytecode) diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt index 4695c3600..14453afaf 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/DescriptorUtilsTest.kt @@ -20,7 +20,6 @@ import org.junit.Test import kotlin.test.assertEquals class DescriptorUtilsTest { - @Test fun testClassDescriptor() { assertEquals("V", java.lang.Void::class.javaPrimitiveType?.descriptor) @@ -38,33 +37,47 @@ class DescriptorUtilsTest { @Test fun testExtractTypeDescriptors() { - val testCases = listOf( - Triple( - String::class.java.getMethod("equals", Object::class.java), - listOf("Ljava/lang/Object;"), - "Z", - ), - Triple( - String::class.java.getMethod("regionMatches", Boolean::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java, Int::class.javaPrimitiveType, Integer::class.javaPrimitiveType), - listOf("Z", "I", "Ljava/lang/String;", "I", "I"), - "Z", - ), - Triple( - String::class.java.getMethod("getChars", Integer::class.javaPrimitiveType, Int::class.javaPrimitiveType, CharArray::class.java, Int::class.javaPrimitiveType), - listOf("I", "I", "[C", "I"), - "V", - ), - Triple( - String::class.java.getMethod("subSequence", Integer::class.javaPrimitiveType, Integer::class.javaPrimitiveType), - listOf("I", "I"), - "Ljava/lang/CharSequence;", - ), - Triple( - String::class.java.getConstructor(), - emptyList(), - "V", - ), - ) + val testCases = + listOf( + Triple( + String::class.java.getMethod("equals", Object::class.java), + listOf("Ljava/lang/Object;"), + "Z", + ), + Triple( + String::class.java.getMethod( + "regionMatches", + Boolean::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + String::class.java, + Int::class.javaPrimitiveType, + Integer::class.javaPrimitiveType, + ), + listOf("Z", "I", "Ljava/lang/String;", "I", "I"), + "Z", + ), + Triple( + String::class.java.getMethod( + "getChars", + Integer::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + CharArray::class.java, + Int::class.javaPrimitiveType, + ), + listOf("I", "I", "[C", "I"), + "V", + ), + Triple( + String::class.java.getMethod("subSequence", Integer::class.javaPrimitiveType, Integer::class.javaPrimitiveType), + listOf("I", "I"), + "Ljava/lang/CharSequence;", + ), + Triple( + String::class.java.getConstructor(), + emptyList(), + "V", + ), + ) for ((executable, parameterDescriptors, returnTypeDescriptor) in testCases) { val descriptor = executable.descriptor assertEquals(extractParameterTypeDescriptors(descriptor), parameterDescriptors) diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt index d4cc35cbc..e100ea744 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt @@ -20,22 +20,26 @@ import java.io.FileOutputStream object PatchTestUtils { @JvmStatic - fun classToBytecode(targetClass: Class<*>): ByteArray { - return ClassLoader + fun classToBytecode(targetClass: Class<*>): ByteArray = + ClassLoader .getSystemClassLoader() .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! .use { it.readBytes() } - } @JvmStatic - fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { - return BytecodeClassLoader(name, bytecode).loadClass(name) - } + fun bytecodeToClass( + name: String, + bytecode: ByteArray, + ): Class<*> = BytecodeClassLoader(name, bytecode).loadClass(name) @JvmStatic - fun dumpBytecode(outDir: String, name: String, originalBytecode: ByteArray) { + fun dumpBytecode( + outDir: String, + name: String, + originalBytecode: ByteArray, + ) { FileOutputStream("$outDir/$name.class").use { fos -> fos.write(originalBytecode) } } @@ -43,8 +47,10 @@ object PatchTestUtils { * A ClassLoader that dynamically loads a single specified class from byte code and delegates all other class loads to * its own ClassLoader. */ - class BytecodeClassLoader(val className: String, private val classBytecode: ByteArray) : - ClassLoader(BytecodeClassLoader::class.java.classLoader) { + class BytecodeClassLoader( + val className: String, + private val classBytecode: ByteArray, + ) : ClassLoader(BytecodeClassLoader::class.java.classLoader) { override fun loadClass(name: String): Class<*> { if (name != className) { return super.loadClass(name) @@ -54,7 +60,10 @@ object PatchTestUtils { } } -fun assertSelfCheck(target: DynamicTestContract, shouldPass: Boolean = true) { +fun assertSelfCheck( + target: DynamicTestContract, + shouldPass: Boolean = true, +) { val results = target.selfCheck() for ((test, passed) in results) { if (shouldPass) { diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt index b2de1fe3e..1c16e79f8 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -21,17 +21,16 @@ import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File -private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract { - return ReplaceHooksTarget() -} +private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract = ReplaceHooksTarget() private fun getNoHooksReplaceHooksTargetInstance(): ReplaceHooksTargetContract { val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java) // Let the bytecode pass through the hooking logic, but don't apply any hooks. - val patchedBytecode = HookInstrumentor(emptyList(), false, null).instrument( - ReplaceHooksTarget::class.java.name.replace('.', '/'), - originalBytecode, - ) + val patchedBytecode = + HookInstrumentor(emptyList(), false, null).instrument( + ReplaceHooksTarget::class.java.name.replace('.', '/'), + originalBytecode, + ) val patchedClass = bytecodeToClass(ReplaceHooksTarget::class.java.name, patchedBytecode) return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract } @@ -39,11 +38,12 @@ private fun getNoHooksReplaceHooksTargetInstance(): ReplaceHooksTargetContract { private fun getPatchedReplaceHooksTargetInstance(classWithHooksEnabledField: Class<*>?): ReplaceHooksTargetContract { val originalBytecode = classToBytecode(ReplaceHooksTarget::class.java) val hooks = Hooks.loadHooks(emptyList(), setOf(ReplaceHooks::class.java.name)).first().hooks - val patchedBytecode = HookInstrumentor( - hooks, - false, - classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), - ).instrument(ReplaceHooksTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + HookInstrumentor( + hooks, + false, + classWithHooksEnabledField = classWithHooksEnabledField?.name?.replace('.', '/'), + ).instrument(ReplaceHooksTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${ReplaceHooksTarget::class.java.simpleName}.class").writeBytes(originalBytecode) @@ -53,7 +53,6 @@ private fun getPatchedReplaceHooksTargetInstance(classWithHooksEnabledField: Cla } class ReplaceHooksPatchTest { - @Test fun testOriginal() { assertSelfCheck(getOriginalReplaceHooksTargetInstance(), false) diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt index 36c699911..074486270 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt @@ -21,20 +21,19 @@ import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File -private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { - return TraceDataFlowInstrumentationTarget() -} +private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract = TraceDataFlowInstrumentationTarget() private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract { val originalBytecode = classToBytecode(TraceDataFlowInstrumentationTarget::class.java) - val patchedBytecode = TraceDataFlowInstrumentor( - setOf( - InstrumentationType.CMP, - InstrumentationType.DIV, - InstrumentationType.GEP, - ), - MockTraceDataFlowCallbacks::class.java.name.replace('.', '/'), - ).instrument(TraceDataFlowInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) + val patchedBytecode = + TraceDataFlowInstrumentor( + setOf( + InstrumentationType.CMP, + InstrumentationType.DIV, + InstrumentationType.GEP, + ), + MockTraceDataFlowCallbacks::class.java.name.replace('.', '/'), + ).instrument(TraceDataFlowInstrumentationTarget::class.java.name.replace('.', '/'), originalBytecode) // Make the patched class available in bazel-testlogs/.../test.outputs for manual inspection. val outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") File("$outDir/${TraceDataFlowInstrumentationTarget::class.simpleName}.class").writeBytes(originalBytecode) @@ -44,7 +43,6 @@ private fun getInstrumentedInstrumentationTargetInstance(): DynamicTestContract } class TraceDataFlowInstrumentationTest { - @Test fun testOriginal() { MockTraceDataFlowCallbacks.init() @@ -101,7 +99,6 @@ class TraceDataFlowInstrumentationTest { // shortArray[8] == 8 "GEP: 8", "ICMP: 8, 8", - "GEP: 2", "GEP: 3", "GEP: 4", diff --git a/tests/src/test/java/com/example/KotlinStringCompareFuzzer.kt b/tests/src/test/java/com/example/KotlinStringCompareFuzzer.kt index e2a19b783..312b9ee9e 100644 --- a/tests/src/test/java/com/example/KotlinStringCompareFuzzer.kt +++ b/tests/src/test/java/com/example/KotlinStringCompareFuzzer.kt @@ -25,7 +25,8 @@ object KotlinStringCompareFuzzer { @OptIn(ExperimentalEncodingApi::class) fun fuzzerTestOneInput(data: ByteArray) { val text = Base64.encode(data) - if (text.startsWith("aGVsbG8K") && // hello + if (text.startsWith("aGVsbG8K") && + // hello text.endsWith("d29ybGQK") // world ) { throw IOException("Found the secret message!") diff --git a/tests/src/test/java/com/example/KotlinVararg.kt b/tests/src/test/java/com/example/KotlinVararg.kt index b4933c587..e7f82772c 100644 --- a/tests/src/test/java/com/example/KotlinVararg.kt +++ b/tests/src/test/java/com/example/KotlinVararg.kt @@ -16,7 +16,9 @@ package com.example -class KotlinVararg(vararg opts: String) { +class KotlinVararg( + vararg opts: String, +) { private val allOpts = opts.toList().joinToString(", ") fun doStuff() = allOpts From fed0080cee2814856d89b5cbede1e2735cf282ba Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Sat, 18 Jan 2025 11:53:00 +0100 Subject: [PATCH 15/20] Suppress ktlint issues without automatic fixes --- .../com/code_intelligence/jazzer/sanitizers/LdapInjection.kt | 1 + .../code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt | 1 + .../jazzer/instrumentor/BeforeHooksPatchTest.kt | 1 + .../jazzer/instrumentor/ReplaceHooksPatchTest.kt | 1 + 4 files changed, 4 insertions(+) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt index 087c93014..b0c6f1de0 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -50,6 +50,7 @@ object LdapInjection { // Characters to escape in search filter queries private const val FILTER_CHARACTERS = "*()\\\u0000" + @Suppress("ktlint:standard:max-line-length") @MethodHooks( // Single object lookup, possible DN injection MethodHook( diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt index 4273e64b1..fd691f205 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -52,6 +52,7 @@ private fun getPatchedAfterHooksTargetInstance(classWithHooksEnabledField: Class return patchedClass.getDeclaredConstructor().newInstance() as AfterHooksTargetContract } +@Suppress("ktlint:standard:property-naming") class AfterHooksPatchTest { @Test fun testOriginal() { diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt index 60d22aaf4..a5fb4c68a 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -52,6 +52,7 @@ private fun getPatchedBeforeHooksTargetInstance(classWithHooksEnabledField: Clas return patchedClass.getDeclaredConstructor().newInstance() as BeforeHooksTargetContract } +@Suppress("ktlint:standard:property-naming") class BeforeHooksPatchTest { @Test fun testOriginal() { diff --git a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt index 1c16e79f8..072571f57 100644 --- a/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt +++ b/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -52,6 +52,7 @@ private fun getPatchedReplaceHooksTargetInstance(classWithHooksEnabledField: Cla return patchedClass.getDeclaredConstructor().newInstance() as ReplaceHooksTargetContract } +@Suppress("ktlint:standard:property-naming") class ReplaceHooksPatchTest { @Test fun testOriginal() { From 9e0aa752438189851ae497f8eaf0105de62f48aa Mon Sep 17 00:00:00 2001 From: tallison Date: Tue, 17 Dec 2024 16:48:58 -0500 Subject: [PATCH 16/20] initial path traversal sanitizer --- sanitizers/sanitizers.bzl | 1 + .../jazzer/sanitizers/BUILD.bazel | 7 + .../jazzer/sanitizers/FilePathTraversal.java | 352 ++++++++++++++++++ .../src/test/java/com/example/BUILD.bazel | 13 + .../java/com/example/FilePathTraversal.java | 49 +++ 5 files changed, 422 insertions(+) create mode 100644 sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java create mode 100644 sanitizers/src/test/java/com/example/FilePathTraversal.java diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl index 0ac523fe2..ffa30114a 100644 --- a/sanitizers/sanitizers.bzl +++ b/sanitizers/sanitizers.bzl @@ -21,6 +21,7 @@ _sanitizer_class_names = [ "ClojureLangHooks", "Deserialization", "ExpressionLanguageInjection", + "FilePathTraversal", "LdapInjection", "NamingContextLookup", "OsCommandInjection", diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel index 2498c8df1..60d2e02e9 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel @@ -9,6 +9,12 @@ java_library( deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], ) +java_library( + name = "file_path_traversal", + srcs = ["FilePathTraversal.java"], + deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], +) + java_library( name = "regex_roadblocks", srcs = ["RegexRoadblocks.java"], @@ -58,6 +64,7 @@ kt_jvm_library( visibility = ["//sanitizers:__pkg__"], runtime_deps = [ ":clojure_lang_hooks", + ":file_path_traversal", ":regex_roadblocks", ":script_engine_injection", ":server_side_request_forgery", diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java new file mode 100644 index 000000000..9ec5ea69f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java @@ -0,0 +1,352 @@ +package com.code_intelligence.jazzer.sanitizers; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; + +import java.io.IOException; +import java.io.File; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * This tests for a file read or write of a specific file name AND + * whether that file is in an allowed directory or a descendant. + *

+ * This checks only for literal, absolute, normalized paths. It does not process symbolic links. + *

+ * This sanitizer will only trigger if {@link FilePathTraversal#ALLOWED_DIRS_KEY} is + * set as an environment variable. If that is not set, this sanitizer is a no-op. + *

+ * This does not check for reading metadata from files outside of the allowed directories. + */ +public class FilePathTraversal { + public static final String FILE_NAME_ENV_KEY = "JAZZER_FILE_SYSTEM_TRAVERSAL_FILE_NAME"; + public static final String ALLOWED_DIRS_KEY = "jazzer.fs_allowed_dirs"; + public static final String DEFAULT_SENTINEL = "jazzer-traversal"; + public static final String SENTINEL = + (System.getenv(FILE_NAME_ENV_KEY) == null || + System.getenv(FILE_NAME_ENV_KEY).trim().length() == 0) ? + DEFAULT_SENTINEL : System.getenv(FILE_NAME_ENV_KEY); + + //intentionally skipping createLink and createSymbolicLink + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectory" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectories" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createFile" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempDirectory" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempFile" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "delete" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "deleteIfExists" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "lines" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newByteChannel" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedWriter" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readString" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllBytes" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllLines" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readSymbolicLink" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "write" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "writeString" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newInputStream" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newOutputStream" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.probeContentType", + targetMethod = "open" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.channels.FileChannel", + targetMethod = "open" + ) + public static void pathFirstArgHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + Object argObj = arguments[0]; + if (argObj instanceof Path) { + checkPath((Path)argObj); + } + } + } + + /** + * Checks to confirm that a path that is read from or written to + * is in an allowed directory. + * + * @param method + * @param thisObject + * @param arguments + * @param hookId + */ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "copy" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "mismatch" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "move" + ) + public static void copyMismatchMvHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 1) { + Object from = arguments[0]; + if (from instanceof Path) { + checkPath((Path) from); + } + Object to = arguments[1]; + if (to instanceof Path) { + checkPath((Path) to); + } + } + } + + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileReader", + targetMethod = "" + ) + public static void fileReaderHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileWriter", + targetMethod = "" + ) + public static void fileWriterHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileInputStream", + targetMethod = "" + ) + public static void fileInputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "" + ) + public static void processFileOutputStartHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + Object argObj = arguments[0]; + if (argObj instanceof File) { + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.util.Scanner", + targetMethod = "" + ) + public static void scannerHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof Path) { + checkPath((Path)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "" + ) + public static void fileOutputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof File) { + checkPath((File)argObj); + } else if (argObj instanceof String) { + checkPath((String)argObj); + } + } + } + + private static void checkPath(File f) { + try { + checkPath(f.toPath()); + } catch (InvalidPathException e) { + //TODO: give up -- for now + } + } + + private static void checkPath(String s) { + try { + checkPath(Paths.get(s)); + } catch (InvalidPathException e) { + checkPath(new File(s)); + } + } + + private static void checkPath(Path p) { + if (p.getFileName().toString().equals(SENTINEL) && ! isAllowed(p)) { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueCritical("File path traversal: " + p)); + } + } + + private static boolean isAllowed(Path candidate) { + String allowedDirString = System.getProperty(ALLOWED_DIRS_KEY); + + if (allowedDirString == null || allowedDirString.trim().length() == 0) { + return true; + } + + Path candidateNormalized = candidate.toAbsolutePath().normalize(); + for (String pString : allowedDirString.split(",")) { + Path allowedNormalized = Paths.get(pString).toAbsolutePath().normalize(); + if (candidateNormalized.startsWith(allowedNormalized) && + ! candidateNormalized.equals(allowedNormalized)) { + return true; + } + } + return false; + } + +} + diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 2233c5350..189fa0172 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -58,6 +58,19 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "FilePathTraversal", + srcs = [ + "FilePathTraversal.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + ], + target_class = "com.example.FilePathTraversal", + #not clear why reproducer doesn't work TODO -- fix this + verify_crash_reproducer = False, +) + java_fuzz_target_test( name = "OsCommandInjectionProcessBuilder", srcs = [ diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java new file mode 100644 index 000000000..1e21a47d1 --- /dev/null +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -0,0 +1,49 @@ +// +// 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.example; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; + +public class FilePathTraversal { + + static { + System.setProperty("jazzer.fs_allowed_dirs", + System.getProperty("java.io.tmpdir") + "," + + "/a/b/c/allowed"); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) { + String data = fuzzedDataProvider.consumeString(100); + String path = fuzzedDataProvider.consumeProbabilityDouble() < 0.1 ? + "/a/b/d/e/jazzer-traversal" : data; + try { + Path p = Paths.get(path); + try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { + r.read(); + } catch (IOException ignored) { + //swallow + } + } catch (InvalidPathException ignored) { + //swallow + } + } +} From 999ebcbce74a5c63eae730827b76fb4c3d73d816 Mon Sep 17 00:00:00 2001 From: tallison Date: Wed, 18 Dec 2024 08:36:05 -0500 Subject: [PATCH 17/20] add/fix license headers --- .../jazzer/sanitizers/FilePathTraversal.java | 15 +++++++++++ .../java/com/example/FilePathTraversal.java | 27 ++++++++++--------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java index 9ec5ea69f..b7f5d1fff 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024 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.sanitizers; import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical; diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java index 1e21a47d1..f8718237a 100644 --- a/sanitizers/src/test/java/com/example/FilePathTraversal.java +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -1,15 +1,18 @@ -// -// 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. +/* + * Copyright 2024 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.example; From 9dfc976c44f75603705e8e3e987d121bef782311 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Tue, 7 Jan 2025 10:13:30 -0500 Subject: [PATCH 18/20] Update sanitizers/src/test/java/com/example/FilePathTraversal.java Co-authored-by: Peter Samarin --- .../java/com/example/FilePathTraversal.java | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java index f8718237a..441b5259f 100644 --- a/sanitizers/src/test/java/com/example/FilePathTraversal.java +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -16,6 +16,9 @@ package com.example; +import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -24,29 +27,22 @@ import java.nio.file.Path; import java.nio.file.Paths; -import com.code_intelligence.jazzer.api.FuzzedDataProvider; +public class FilePathTraversal { -public class FilePathTraversal { - - static { - System.setProperty("jazzer.fs_allowed_dirs", - System.getProperty("java.io.tmpdir") + "," + - "/a/b/c/allowed"); - } - - public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) { - String data = fuzzedDataProvider.consumeString(100); - String path = fuzzedDataProvider.consumeProbabilityDouble() < 0.1 ? - "/a/b/d/e/jazzer-traversal" : data; - try { - Path p = Paths.get(path); - try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { - r.read(); - } catch (IOException ignored) { - //swallow - } - } catch (InvalidPathException ignored) { - //swallow - } + public static void fuzzerTestOneInput( + @WithUtf8Length(max = 100) @NotNull String pathFromFuzzer, + @NotNull @DoubleInRange(min = 0.0, max = 1.0) Double fixedPathProbability) { + // Slow down the fuzzer a bit, otherwise it finds file path traversal way too quickly! + String path = fixedPathProbability < 0.95 ? "/a/b/c/fixed-path" : pathFromFuzzer; + try { + Path p = Paths.get(path); + try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { + r.read(); + } catch (IOException ignored) { + // swallow + } + } catch (InvalidPathException ignored) { + // swallow } + } } From 0b1cb2b35e571bff5c4a15c02d5039f593f1f5c0 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Tue, 7 Jan 2025 10:14:28 -0500 Subject: [PATCH 19/20] Update sanitizers/src/test/java/com/example/BUILD.bazel Co-authored-by: Peter Samarin --- sanitizers/src/test/java/com/example/BUILD.bazel | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 189fa0172..a7d63c403 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -69,6 +69,10 @@ java_fuzz_target_test( target_class = "com.example.FilePathTraversal", #not clear why reproducer doesn't work TODO -- fix this verify_crash_reproducer = False, + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + ], +) ) java_fuzz_target_test( From cfb5134006591095ee317e00befbe69adace186e Mon Sep 17 00:00:00 2001 From: tallison Date: Tue, 14 Jan 2025 16:49:54 -0500 Subject: [PATCH 20/20] updates --- .../jazzer/sanitizers/FilePathTraversal.java | 596 +++++++++--------- .../example/AbsoluteFilePathTraversal.java | 52 ++ .../src/test/java/com/example/BUILD.bazel | 17 +- 3 files changed, 357 insertions(+), 308 deletions(-) create mode 100644 sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java index b7f5d1fff..303125162 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java @@ -19,349 +19,331 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.Jazzer; import com.code_intelligence.jazzer.api.MethodHook; - -import java.io.IOException; import java.io.File; import java.lang.invoke.MethodHandle; -import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.logging.Level; +import java.util.logging.Logger; /** - * This tests for a file read or write of a specific file name AND - * whether that file is in an allowed directory or a descendant. - *

- * This checks only for literal, absolute, normalized paths. It does not process symbolic links. - *

- * This sanitizer will only trigger if {@link FilePathTraversal#ALLOWED_DIRS_KEY} is - * set as an environment variable. If that is not set, this sanitizer is a no-op. - *

- * This does not check for reading metadata from files outside of the allowed directories. + * This tests for a file read or write of a specific file path whether relative or absolute. + * + *

This checks only for literal, absolute, normalized paths. It does not process symbolic links. + * + *

The default target is {@link FilePathTraversal#DEFAULT_TARGET_STRING} + * + *

Users may customize a customize the target by setting the full path in the environment + * variable {@link FilePathTraversal#FILE_PATH_TARGET_KEY} + * + *

This does not currently check for reading metadata from the target file. */ public class FilePathTraversal { - public static final String FILE_NAME_ENV_KEY = "JAZZER_FILE_SYSTEM_TRAVERSAL_FILE_NAME"; - public static final String ALLOWED_DIRS_KEY = "jazzer.fs_allowed_dirs"; - public static final String DEFAULT_SENTINEL = "jazzer-traversal"; - public static final String SENTINEL = - (System.getenv(FILE_NAME_ENV_KEY) == null || - System.getenv(FILE_NAME_ENV_KEY).trim().length() == 0) ? - DEFAULT_SENTINEL : System.getenv(FILE_NAME_ENV_KEY); + public static final String FILE_PATH_TARGET_KEY = "jazzer.file_path_traversal_target"; + public static final String DEFAULT_TARGET_STRING = "../jazzer-traversal"; - //intentionally skipping createLink and createSymbolicLink + private static final Logger LOG = Logger.getLogger(FilePathTraversal.class.getName()); - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createDirectory" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createDirectories" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createFile" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createTempDirectory" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createTempFile" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "delete" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "deleteIfExists" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "lines" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newByteChannel" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedReader" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedWriter" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readString" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedReader" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readAllBytes" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readAllLines" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readSymbolicLink" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "write" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "writeString" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newInputStream" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newOutputStream" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.probeContentType", - targetMethod = "open" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.channels.FileChannel", - targetMethod = "open" - ) - public static void pathFirstArgHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - Object argObj = arguments[0]; - if (argObj instanceof Path) { - checkPath((Path)argObj); - } - } - } + private static Path RELATIVE_TARGET; + private static Path ABSOLUTE_TARGET; + private static boolean IS_DISABLED = false; + private static boolean IS_SET_UP = false; - /** - * Checks to confirm that a path that is read from or written to - * is in an allowed directory. - * - * @param method - * @param thisObject - * @param arguments - * @param hookId - */ - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "copy" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "mismatch" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "move" - ) - public static void copyMismatchMvHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 1) { - Object from = arguments[0]; - if (from instanceof Path) { - checkPath((Path) from); - } - Object to = arguments[1]; - if (to instanceof Path) { - checkPath((Path) to); - } - } + private static void setUp() { + String customTarget = System.getProperty(FILE_PATH_TARGET_KEY); + if (customTarget != null && !customTarget.isEmpty()) { + LOG.log(Level.FINE, "custom target loaded: " + customTarget); + setTargets(customTarget); + } else { + // check that this isn't being run at the root directory + Path cwd = Paths.get(".").toAbsolutePath(); + if (cwd.getParent() == null) { + LOG.warning( + "Can't run from the root directory with the default target. " + + "The FilePathTraversal sanitizer is disabled."); + IS_DISABLED = true; + } + setTargets(DEFAULT_TARGET_STRING); } + } - - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileReader", - targetMethod = "" - ) - public static void fileReaderHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + private static void setTargets(String targetPath) { + Path p = Paths.get(targetPath); + Path pwd = Paths.get("."); + if (p.isAbsolute()) { + ABSOLUTE_TARGET = p.toAbsolutePath().normalize(); + RELATIVE_TARGET = pwd.toAbsolutePath().relativize(ABSOLUTE_TARGET).normalize(); + } else { + ABSOLUTE_TARGET = pwd.resolve(p).toAbsolutePath().normalize(); + RELATIVE_TARGET = p.normalize(); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileWriter", - targetMethod = "" - ) - public static void fileWriterHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + // intentionally skipping createLink and createSymbolicLink + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectory") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectories") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createFile") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempDirectory") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempFile") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "delete") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "deleteIfExists") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "lines") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newByteChannel") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedWriter") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readString") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllBytes") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllLines") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readSymbolicLink") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "write") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "writeString") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newInputStream") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newOutputStream") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.probeContentType", + targetMethod = "open") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.channels.FileChannel", + targetMethod = "open") + public static void pathFirstArgHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + Object argObj = arguments[0]; + if (argObj instanceof Path) { + checkPath((Path) argObj, hookId); + } } + } - - - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileInputStream", - targetMethod = "" - ) - public static void fileInputStreamHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + /** + * Checks to confirm that a path that is read from or written to is in an allowed directory. + * + * @param method + * @param thisObject + * @param arguments + * @param hookId + */ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "copy") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "mismatch") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "move") + public static void copyMismatchMvHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 1) { + Object from = arguments[0]; + if (from instanceof Path) { + checkPath((Path) from, hookId); + } + Object to = arguments[1]; + if (to instanceof Path) { + checkPath((Path) to, hookId); + } } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileOutputStream", - targetMethod = "" - ) - public static void processFileOutputStartHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - Object argObj = arguments[0]; - if (argObj instanceof File) { - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileReader", + targetMethod = "") + public static void fileReaderHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.util.Scanner", - targetMethod = "" - ) - public static void scannerHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileWriter", + targetMethod = "") + public static void fileWriterHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); + } + } - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof Path) { - checkPath((Path)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileInputStream", + targetMethod = "") + public static void fileInputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileOutputStream", - targetMethod = "" - ) - public static void fileOutputStreamHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "") + public static void processFileOutputStartHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); + } + } - Object argObj = arguments[0]; - if (argObj instanceof File) { - checkPath((File)argObj); - } else if (argObj instanceof String) { - checkPath((String)argObj); - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.util.Scanner", + targetMethod = "") + public static void scannerHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - private static void checkPath(File f) { - try { - checkPath(f.toPath()); - } catch (InvalidPathException e) { - //TODO: give up -- for now - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "") + public static void fileOutputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - private static void checkPath(String s) { - try { - checkPath(Paths.get(s)); - } catch (InvalidPathException e) { - checkPath(new File(s)); - } + private static void checkObj(Object obj, int hookId) { + if (obj instanceof String) { + checkString((String) obj, hookId); + } else if (obj instanceof Path) { + checkPath((Path) obj, hookId); + } else if (obj instanceof File) { + checkFile((File) obj, hookId); } + } - private static void checkPath(Path p) { - if (p.getFileName().toString().equals(SENTINEL) && ! isAllowed(p)) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueCritical("File path traversal: " + p)); - } + private static void checkPath(Path p, int hookId) { + check(p); + Path normalized = p.normalize(); + if (p.isAbsolute()) { + Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId); + } else { + Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId); } + } - private static boolean isAllowed(Path candidate) { - String allowedDirString = System.getProperty(ALLOWED_DIRS_KEY); + private static void checkFile(File f, int hookId) { + try { + check(f.toPath()); + } catch (InvalidPathException e) { + // TODO: give up -- for now + return; + } + Path normalized = f.toPath().normalize(); + if (normalized.isAbsolute()) { + Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId); + } else { + Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId); + } + } - if (allowedDirString == null || allowedDirString.trim().length() == 0) { - return true; - } + private static void checkString(String s, int hookId) { + try { + check(Paths.get(s)); + } catch (InvalidPathException e) { + checkFile(new File(s), hookId); + // TODO -- give up for now + return; + } + Path normalized = Paths.get(s); + if (normalized.isAbsolute()) { + Jazzer.guideTowardsEquality(s, ABSOLUTE_TARGET.toString(), hookId); + } else { + Jazzer.guideTowardsEquality(s, RELATIVE_TARGET.toString(), hookId); + } + } - Path candidateNormalized = candidate.toAbsolutePath().normalize(); - for (String pString : allowedDirString.split(",")) { - Path allowedNormalized = Paths.get(pString).toAbsolutePath().normalize(); - if (candidateNormalized.startsWith(allowedNormalized) && - ! candidateNormalized.equals(allowedNormalized)) { - return true; - } - } - return false; + private static void check(Path p) { + // super lazy initialization -- race condition with unit test if this is set in a static block + synchronized (LOG) { + if (!IS_SET_UP) { + setUp(); + IS_SET_UP = true; + } + } + if (IS_DISABLED) { + return; } + if (p.toAbsolutePath().normalize().equals(ABSOLUTE_TARGET)) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("File path traversal: " + p)); + } + } } - diff --git a/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java new file mode 100644 index 000000000..f6a7648f5 --- /dev/null +++ b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.example; + +import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class AbsoluteFilePathTraversal { + static { + System.setProperty("jazzer.file_path_traversal_target", "/custom/path/jazzer-traversal"); + } + + public static void fuzzerTestOneInput( + @WithUtf8Length(max = 100) @NotNull String pathFromFuzzer, + @NotNull @DoubleInRange(min = 0.0, max = 1.0) Double fixedPathProbability) { + // Slow down the fuzzer a bit, otherwise it finds file path traversal way too quickly! + String path = fixedPathProbability < 0.95 ? "/a/b/c/fixed-path" : pathFromFuzzer; + + try { + Path p = Paths.get(path); + try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { + r.read(); + } catch (IOException ignored) { + // swallow + } + } catch (InvalidPathException ignored) { + // swallow + } + } +} diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index a7d63c403..695380c79 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -58,6 +58,22 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "AbsoluteFilePathTraversal", + srcs = [ + "AbsoluteFilePathTraversal.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + ], + target_class = "com.example.AbsoluteFilePathTraversal", + #not clear why reproducer doesn't work TODO -- fix this + verify_crash_reproducer = False, + deps = [ + "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", + ], +) + java_fuzz_target_test( name = "FilePathTraversal", srcs = [ @@ -73,7 +89,6 @@ java_fuzz_target_test( "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", ], ) -) java_fuzz_target_test( name = "OsCommandInjectionProcessBuilder",