diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:09:17 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:09:17 +0000 |
commit | 9c1babbb2eff7f4ea133f4e049ecc340d0a66521 (patch) | |
tree | 3243e58e56d3a8dee60c582a2c917d18edc9bd5a | |
parent | a74c14e5721cfd85dd0d0ebc3789ac0657564b7b (diff) | |
parent | ba37c2e361c2ba91bacc47fcae5383c52e50f6be (diff) | |
download | jazzer-api-android14-mainline-sdkext-release.tar.gz |
Snap for 10453563 from ba37c2e361c2ba91bacc47fcae5383c52e50f6be to mainline-sdkext-releaseaml_sdk_341510000aml_sdk_341410000aml_sdk_341110080aml_sdk_341110000aml_sdk_341010000aml_sdk_340912010android14-mainline-sdkext-release
Change-Id: I75e517760138919b7691d8120888c254d8a9f04f
241 files changed, 11619 insertions, 5332 deletions
@@ -4,13 +4,7 @@ build --enable_platform_specific_config build -c opt # C/C++ -# Only relevant for tests and their dependencies. Everything that external -# repositories can reference must build without this, e.g., by using a -# transition. -build:linux --cxxopt='-std=c++17' -build:macos --cxxopt='-std=c++17' -build:windows --cxxopt='/std:c++17' -build --repo_env=CC=clang +common --repo_env=CC=clang build --incompatible_enable_cc_toolchain_resolution # Requires a relatively modern clang. build:ci --features=layering_check @@ -31,16 +25,34 @@ run:windows --noincompatible_strict_action_env # Toolchain # Since the toolchain is conditional on OS and architecture, set it on the particular GitHub Action. +build:toolchain --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 build:toolchain --//third_party:toolchain +# Forward debug variables to tests +test --test_env=JAZZER_AUTOFUZZ_DEBUG +test --test_env=JAZZER_REFLECTION_DEBUG + # CI tests (not using the toolchain to test OSS-Fuzz & local compatibility) +test:ci --test_env=JAZZER_CI=1 build:ci --bes_results_url=https://app.buildbuddy.io/invocation/ -build:ci --bes_backend=grpcs://cloud.buildbuddy.io -build:ci --remote_cache=grpcs://cloud.buildbuddy.io +build:ci --bes_backend=grpcs://remote.buildbuddy.io +build:ci --remote_cache=grpcs://remote.buildbuddy.io build:ci --remote_timeout=3600 # Maven publishing (local only, requires GPG signature) build:maven --config=toolchain build:maven --stamp build:maven --define "maven_repo=https://oss.sonatype.org/service/local/staging/deploy/maven2" -build:maven --java_runtime_version=localjdk_8 +build:maven --java_runtime_version=local_jdk_8 + +# Generic coverage configuration taken from https://github.com/fmeum/rules_jni +coverage --combined_report=lcov +coverage --experimental_use_llvm_covmap +coverage --experimental_generate_llvm_lcov +coverage --repo_env=CC=clang +coverage --repo_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1 +coverage --repo_env=GCOV=llvm-profdata + +# Instrument all source files of non-test targets matching at least one of these regexes. +coverage --instrumentation_filter=^//agent/src/main[:/],^//driver:,^//sanitizers/src/main[:/] +coverage --test_tag_filters=-no-coverage diff --git a/.bazelversion b/.bazelversion index af8c8ec7..bddfde6e 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -4.2.2 +5.3.0rc1 diff --git a/.github/BUILD.bazel b/.github/BUILD.bazel new file mode 100644 index 00000000..ee17cccf --- /dev/null +++ b/.github/BUILD.bazel @@ -0,0 +1,26 @@ +# Extracted on 2022-01-05 as described in +# https://www.smileykeith.com/2021/03/08/locking-xcode-in-bazel/ + +package(default_visibility = ["//visibility:public"]) + +xcode_version( + name = "version13_1_0_13A1030d", + aliases = [ + "13.1.0", + "13.1", + "13.1.0.13A1030d", + ], + default_ios_sdk_version = "15.0", + default_macos_sdk_version = "12.0", + default_tvos_sdk_version = "15.0", + default_watchos_sdk_version = "8.0", + version = "13.1.0.13A1030d", +) + +xcode_config( + name = "host_xcodes", + default = ":version13_1_0_13A1030d", + versions = [ + ":version13_1_0_13A1030d", + ], +) diff --git a/.github/scripts/echoBuildBuddyConfig.sh b/.github/scripts/echoBuildBuddyConfig.sh new file mode 100755 index 00000000..7953549b --- /dev/null +++ b/.github/scripts/echoBuildBuddyConfig.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ -n "${1}" ]; then + echo "BUILD_BUDDY_CONFIG=--config=ci --remote_header=x-buildbuddy-api-key=${1}"; +else + echo ""; +fi diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index c32aaff1..f410ef4c 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -28,8 +28,8 @@ jobs: sudo add-apt-repository 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-13 main' sudo apt-get install clang-format-13 curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.42.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/bin/ktlint - go get -u github.com/google/addlicense - go get github.com/bazelbuild/buildtools/buildifier + go install github.com/google/addlicense@latest + go install github.com/bazelbuild/buildtools/buildifier@latest - name: Run format.sh and print changes run: | diff --git a/.github/workflows/oss-fuzz.yml b/.github/workflows/oss-fuzz.yml new file mode 100644 index 00000000..2c6bbf5e --- /dev/null +++ b/.github/workflows/oss-fuzz.yml @@ -0,0 +1,30 @@ +name: OSS-Fuzz build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + + workflow_dispatch: + +jobs: + + oss_fuzz: + runs-on: ubuntu-20.04 + container: gcr.io/oss-fuzz-base/base-builder-jvm + + steps: + - name: Adding github workspace as safe directory + # See issue https://github.com/actions/checkout/issues/760 + run: git config --global --add safe.directory $GITHUB_WORKSPACE + + - uses: actions/checkout@v2 + + - name: Build Jazzer + # Keep in sync with https://github.com/google/oss-fuzz/blob/221b39181a372ff16c0c813c5963a08aa58f19e2/infra/base-images/base-builder/install_java.sh#L33. + run: bazel build --java_runtime_version=local_jdk_15 -c opt --cxxopt="-stdlib=libc++" --linkopt=-lc++ //agent:jazzer_agent_deploy.jar //driver:jazzer_driver //driver:jazzer_driver_asan //driver:jazzer_driver_ubsan //agent:jazzer_api_deploy.jar + + - name: Test Jazzer build + # Keep in sync with https://github.com/google/oss-fuzz/blob/221b39181a372ff16c0c813c5963a08aa58f19e2/infra/base-images/base-builder/install_java.sh#L35-L36. + run: "test -f bazel-bin/agent/jazzer_agent_deploy.jar && test -f bazel-bin/driver/jazzer_driver && test -f bazel-bin/driver/jazzer_driver_asan && test -f bazel-bin/driver/jazzer_driver_ubsan && test -f bazel-bin/agent/jazzer_api_deploy.jar" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2d5566d..2cbfbaae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,20 +4,20 @@ on: workflow_dispatch: jobs: - build_release: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-10.15, windows-2016] + # Keep arch names in sync with replayer download and merge + os: [ubuntu-latest, macos-10.15, windows-2019] include: - os: ubuntu-latest arch: "linux" bazel_args: "--config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux" - os: macos-10.15 arch: "macos-x86_64" - bazel_args: "--config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-darwin" - - os: windows-2016 + bazel_args: "--config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-darwin --xcode_version_config=//.github:host_xcodes" + - os: windows-2019 arch: "windows" bazel_args: "" @@ -29,9 +29,13 @@ jobs: with: java-version: 8 + - name: Set Build Buddy config + run: .github/scripts/echoBuildBuddyConfig.sh ${{ secrets.BUILDBUDDY_API_KEY }} >> $GITHUB_ENV + shell: bash + - name: Build run: | - bazelisk build --config=ci --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_API_KEY }} --java_runtime_version=localjdk_${{ matrix.jdk }} ${{ matrix.bazel_args }} //agent/src/main/java/com/code_intelligence/jazzer/replay:Replayer_deploy.jar //:jazzer_release + bazelisk build ${{env.BUILD_BUDDY_CONFIG}} --java_runtime_version=local_jdk_8 ${{ matrix.bazel_args }} //agent/src/main/java/com/code_intelligence/jazzer/replay:Replayer_deploy.jar //:jazzer_release cp -L bazel-bin/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer_deploy.jar replayer.jar cp -L bazel-bin/jazzer_release.tar.gz release-${{ matrix.arch }}.tar.gz @@ -55,8 +59,8 @@ jobs: - name: Download macOS jar uses: actions/download-artifact@v2 with: - name: replayer_darwin - path: replayer_darwin + name: replayer_macos-x86_64 + path: replayer_macos-x86_64 - name: Download Linux jar uses: actions/download-artifact@v2 @@ -73,7 +77,7 @@ jobs: - name: Merge jars run: | mkdir merged - unzip -o replayer_darwin/replayer.jar -d merged + unzip -o replayer_macos-x86_64/replayer.jar -d merged unzip -o replayer_linux/replayer.jar -d merged unzip -o replayer_windows/replayer.jar -d merged jar cvmf merged/META-INF/MANIFEST.MF replayer.jar -C merged . @@ -83,4 +87,3 @@ jobs: with: name: replayer path: replayer.jar - diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 2af6f5ec..35334b89 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -15,13 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-11, windows-latest] - jdk: [8, 15, 17] - exclude: - # Only test against JDK 15 with Ubuntu since this is what OSS-Fuzz uses. - - os: macos-11 - jdk: 15 - - os: windows-latest - jdk: 15 + jdk: [8, 17] include: - os: ubuntu-latest arch: "linux" @@ -29,7 +23,7 @@ jobs: - os: macos-11 arch: "macos-x86_64" # Always use the toolchain as UBSan produces linker errors with Apple LLVM 13. - bazel_args: "--config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-darwin" + bazel_args: "--config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-darwin --xcode_version_config=//.github:host_xcodes" cache: "/private/var/tmp/bazel-disk" - os: windows-latest arch: "windows" @@ -49,11 +43,15 @@ jobs: path: ${{ matrix.cache }} key: bazel-disk-cache-${{ matrix.arch }}-${{ matrix.jdk }} + - name: Set Build Buddy config + run: .github/scripts/echoBuildBuddyConfig.sh ${{ secrets.BUILDBUDDY_API_KEY }} >> $GITHUB_ENV + shell: bash + - name: Build - run: bazelisk build --config=ci --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_API_KEY }} --disk_cache=${{ matrix.cache }} --java_runtime_version=localjdk_${{ matrix.jdk }} ${{ matrix.bazel_args }} //... + run: bazelisk build ${{env.BUILD_BUDDY_CONFIG}} --java_runtime_version=local_jdk_${{ matrix.jdk }} --disk_cache=${{ matrix.cache }} ${{ matrix.bazel_args }} //... - name: Test - run: bazelisk test --config=ci --remote_header=x-buildbuddy-api-key=${{ secrets.BUILDBUDDY_API_KEY }} --disk_cache=${{ matrix.cache }} --java_runtime_version=localjdk_${{ matrix.jdk }} ${{ matrix.bazel_args }} //... + run: bazelisk test ${{env.BUILD_BUDDY_CONFIG}} --java_runtime_version=local_jdk_${{ matrix.jdk }} --disk_cache=${{ matrix.cache }} ${{ matrix.bazel_args }} //... - name: Upload test logs if: always() @@ -1,3 +1,4 @@ /bazel-* .ijwb .clwb +/coverage @@ -45,10 +45,20 @@ license { ], } -java_library_host { +java_library { name: "jazzer", + host_supported: true, srcs: [ "agent/src/main/java/com/code_intelligence/jazzer/api/*.java", ], visibility: ["//visibility:public"], } + +java_binary { + name: "jazzer_setup", + wrapper: "jazzer_setup.sh", + host_supported: true, + srcs: [ + "JazzerSetup.java", + ], +} diff --git a/BUILD.bazel b/BUILD.bazel index a5ba2f52..5afcd034 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -24,7 +24,7 @@ define_kt_toolchain( pkg_tar( name = "jazzer_release", srcs = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", "//agent:jazzer_api_deploy.jar", "//driver:jazzer_driver", ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 516b32c0..0898a241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ **Note:** Before version 1.0.0, every release may contain breaking changes. +## Version 0.11.0 + +* Feature: Add sanitizer for context lookups +* Feature: Add sanitizer for OS command injection +* Feature: Add sanitizer for regex injection +* Feature: Add sanitizer for LDAP injections +* Feature: Add sanitizer for arbitrary class loading +* Feature: Guide fuzzer to generate proper map lookups keys +* Feature: Generate standalone Java reproducers for autofuzz +* Feature: Hooks targeting interfaces and abstract classes hook all implementations +* Feature: Enable multiple BEFORE and AFTER hooks for the same target +* Feature: Greatly improve performance of coverage instrumentation +* Feature: Improve performance of interactions between Jazzer and libFuzzer +* Feature: Export JaCoCo coverage dump using `--coverage_dump` flag +* Feature: Honor `JAVA_OPTS` +* API: Add `exploreState` to help the fuzzer maximize state coverage +* API: Provide `additionalClassesToHook` field in `MethodHook` annotation to hook dependent classes +* Fix: Synchronize coverage ID generation +* Fix: Support REPLACE hooks for constructors +* Fix: Do not apply REPLACE hooks in Java 6 class files + +This release also includes smaller improvements and bugfixes. + +## Version 0.10.0 + +* **Breaking change**: Use OS-specific classpath separator to split jvm_args +* Feature: Add support to "autofuzz" targets without the need to manually write fuzz targets +* Feature: Add macOS and Windows support +* Feature: Add option to generate coverage report +* Feature: Support multiple hook annotations per hook method +* Feature: Support hooking internal classes +* Feature: Add sanitizer for insecure deserialization +* Feature: Add sanitizer for arbitrary reflective calls +* Feature: Add sanitizer for expression language injection +* Feature: Provide Jazzer and Jazzer Autofuzz docker images +* Feature: Add a stand-alone replayer to reproduce findings +* API: Add `reportFindingFromHook(Throwable finding)` to report findings from hooks +* API: Add `guideTowardsEquality(String current, String target, int id)` and `guideTowardsContainment(String haystack, String needle, int id)` to guide the fuzzer to generate more useful inputs +* API: Add `consume(FuzzedDataProvider data, Class<T> type)` to create an object instance of the given type from the fuzzer input +* API: Add multiple `autofuzz()` methods to invoke given functions with arguments automatically created from the fuzzer input +* Fixed: Prevent dependency version conflicts in fuzzed application by shading internal dependencies +* Fixed: Make initialized `this` object available to `<init>` AFTER hooks +* Fixed: Allow instrumented classes loaded by custom class loaders to find Jazzer internals + +This release also includes smaller improvements and bugfixes. + ## Version 0.9.1 * **Breaking change**: The static `fuzzerTestOneInput` method in a fuzz target now has to return `void` instead of `boolean`. Fuzz targets that previously returned `true` should now throw an exception or use `assert`. diff --git a/JazzerSetup.java b/JazzerSetup.java new file mode 100644 index 00000000..b0624cc5 --- /dev/null +++ b/JazzerSetup.java @@ -0,0 +1,6 @@ +package com.jazzer; +public class JazzerSetup { + public static void main (String[] args) { + System.out.println("Init'd"); + } +}
\ No newline at end of file @@ -12,7 +12,7 @@ third_party { type: GIT value: "https://github.com/CodeIntelligenceTesting/jazzer" } - version: "726cc6bd0c0e26378574f74b712de948e56664ec" - last_upgrade_date { year: 2021 month: 12 day: 15 } + version: "327677ad48bdd3e87794a1fe3c2becc13288e4b7" + last_upgrade_date { year: 2022 month: 9 day: 15 } license_type: NOTICE } @@ -2,4 +2,4 @@ mhahmad@google.com hamzeh@google.com kalder@google.com cobark@google.com -anisassi@google.com +mteffeteller@google.com @@ -12,7 +12,10 @@ It is based on [libFuzzer](https://llvm.org/docs/LibFuzzer.html) and brings many The JVM bytecode is executed inside the fuzzer process, which ensures fast execution speeds and allows seamless fuzzing of native libraries. -Jazzer supports Linux and (experimentally) macOS 10.15 and 11 as well as Windows, all on the x64 architecture. +Jazzer currently supports the following platforms: +* Linux x86_64 +* macOS 10.15+ x86_64 (experimental support for arm64) +* Windows x86_64 ## News: Jazzer available in OSS-Fuzz @@ -20,12 +23,7 @@ Jazzer supports Linux and (experimentally) macOS 10.15 and 11 as well as Windows If you want to learn more about Jazzer and OSS-Fuzz, [watch the FuzzCon 2020 talk](https://www.youtube.com/watch?v=SmH3Ys_k8vA&list=PLI0R_0_8-TV55gJU-UXrOzZoPbVOj1CW6&index=3) by [Abhishek Arya](https://twitter.com/infernosec) and [Fabian Meumertzheim](https://twitter.com/fhenneke). -## Installation - -The preferred way to install Jazzer is to compile it from source using [Bazel](https://bazel.build), but binary distributions for x64 Linux as well as a Docker image are also available. -Note that these binaries might be outdated as Jazzer follows the "Live at Head" philosophy - you should be able to just checkout the latest commit from `main` and build it. - -Support for Jazzer has recently been added to [rules_fuzzing](https://github.com/bazelbuild/rules_fuzzing), the official Bazel rules for fuzzing. See their README for instructions on how to use Jazzer in a Java Bazel project. +## Getting Jazzer ### Using Docker @@ -37,54 +35,35 @@ docker run -v path/containing/the/application:/fuzzing cifuzz/jazzer <arguments> If Jazzer produces a finding, the input that triggered it will be available in the same directory. -### Using Bazel +### Compiling with Bazel + +#### Dependencies Jazzer has the following dependencies when being built from source: +* Bazel 4 or later * JDK 8 or later (e.g. [OpenJDK](https://openjdk.java.net/)) -* [Clang](https://clang.llvm.org/) 9.0 or later (using a recent version is strongly recommended) - -#### Linux +* [Clang](https://clang.llvm.org/) and [LLD](https://lld.llvm.org/) 9.0 or later (using a recent version is strongly recommended) -Jazzer uses [Bazelisk](https://github.com/bazelbuild/bazelisk) to automatically download and install Bazel on Linux. -Building Jazzer from source and running it thus only requires the following assuming the dependencies are installed: +It is recommended to use [Bazelisk](https://github.com/bazelbuild/bazelisk) to automatically download and install Bazel. +Simply download the release binary for your OS and architecture and ensure that it is available in the `PATH`. +The instructions below will assume that this binary is called `bazel` - Bazelisk is a thin wrapper around the actual Bazel binary and can be used interchangeably. -```bash -git clone https://github.com/CodeIntelligenceTesting/jazzer -cd jazzer -# Note the double dash used to pass <arguments> to Jazzer rather than Bazel. -./bazelisk-linux-amd64 run //:jazzer -- <arguments> -``` +#### Compilation -If you prefer to build binaries that can be run without Bazel, use the following command to build your own archive with release binaries: +Assuming the dependencies are installed, build Jazzer from source as follows: ```bash -$ ./bazelisk-linux-amd64 build //:jazzer_release -... -INFO: Found 1 target... -Target //:jazzer_release up-to-date: - bazel-bin/jazzer_release.tar.gz -... -``` - -This will print the path of a `jazzer_release.tar.gz` archive that contains the same binaries that would be part of a release. - -#### macOS - -Since Jazzer does not ship the macOS version of [Bazelisk](https://github.com/bazelbuild/bazelisk), a tool that automatically downloads and installs the correct version of Bazel, download [the most recent release](https://github.com/bazelbuild/bazelisk/releases) of `bazelisk-darwin`. -Afterwards, clone Jazzer and run it via: - -```bash -git clone https://github.com/CodeIntelligenceTesting/jazzer -cd jazzer +$ git clone https://github.com/CodeIntelligenceTesting/jazzer +$ cd jazzer # Note the double dash used to pass <arguments> to Jazzer rather than Bazel. -/path/to/bazelisk-darwin run //:jazzer -- <arguments> +$ bazel run //:jazzer -- <arguments> ``` If you prefer to build binaries that can be run without Bazel, use the following command to build your own archive with release binaries: ```bash -$ /path/to/bazelisk-darwin build //:jazzer_release +$ bazel build //:jazzer_release ... INFO: Found 1 target... Target //:jazzer_release up-to-date: @@ -94,20 +73,27 @@ Target //:jazzer_release up-to-date: This will print the path of a `jazzer_release.tar.gz` archive that contains the same binaries that would be part of a release. +##### macOS + The build may fail with the clang shipped with Xcode. If you encounter issues during the build, add `--config=toolchain` right after `run` or `build` in the `bazelisk` commands above to use a checked-in toolchain that is known to work. +Alternatively, manually install LLVM and set `CC` to the path of LLVM clang. + +#### rules_fuzzing + +Support for Jazzer has recently been added to [rules_fuzzing](https://github.com/bazelbuild/rules_fuzzing), the official Bazel rules for fuzzing. +See their README for instructions on how to use Jazzer in a Java Bazel project. ### Using the provided binaries -Binary releases are available under [Releases](https://github.com/CodeIntelligenceTesting/jazzer/releases) and are built -using an [LLVM 11 Bazel toolchain](https://github.com/CodeIntelligenceTesting/llvm-toolchain). +Binary releases are available under [Releases](https://github.com/CodeIntelligenceTesting/jazzer/releases), +but do not always include the latest changes. The binary distributions of Jazzer consist of the following components: -- `jazzer_driver` - native binary that interfaces between libFuzzer and the JVM fuzz target -- `jazzer_agent_deploy.jar` - Java agent that performs bytecode instrumentation and tracks coverage +- `jazzer` - main binary +- `jazzer_agent_deploy.jar` - Java agent that performs bytecode instrumentation and tracks coverage (automatically loaded by `jazzer`) - `jazzer_api_deploy.jar` - contains convenience methods for creating fuzz targets and defining custom hooks -- `jazzer` - convenience shell script that runs the Jazzer driver with the local JRE shared libraries added to `LD_LIBRARY_PATH` The additional release artifact `examples_deploy.jar` contains most of the examples and can be used to run them without having to build them (see Examples below). @@ -126,8 +112,8 @@ Multiple examples for instructive and real-world Jazzer fuzz targets can be foun A toy example can be run as follows: ```bash -# Using Bazelisk: -./bazelisk-linux-amd64 run //examples:ExampleFuzzer +# Using Bazel: +bazel run //examples:ExampleFuzzer # Using the binary release and examples_deploy.jar: ./jazzer --cp=examples_deploy.jar ``` @@ -274,23 +260,24 @@ engineered to minimize copying and generate both valid and invalid ASCII-only an The Autofuzz mode enables fuzzing arbitrary methods without having to manually create fuzz targets. Instead, Jazzer will attempt to generate suitable and varied inputs to a specified methods using only public API functions available on the classpath. -To use Autofuzz, specify the `--autofuzz` flag and provide a fully qualified method reference, e.g.: +To use Autofuzz, specify the `--autofuzz` flag and provide a fully [qualified method reference](https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.13), e.g.: ``` --autofuzz=org.apache.commons.imaging.Imaging::getBufferedImage ``` -If there are multiple overloads and you want Jazzer to only fuzz one, you can optionally specify the signature of the method to fuzz: +To autofuzz a constructor the `ClassType::new` format can be used. +If there are multiple overloads, and you want Jazzer to only fuzz one, you can optionally specify the signature of the method to fuzz: ``` --autofuzz=org.apache.commons.imaging.Imaging::getBufferedImage(java.io.InputStream,java.util.Map) ``` The format of the signature agrees with that obtained from the part after the `#` of the link to the Javadocs for the particular method. -Under the hood, jazzer tries various ways of creating objects from the fuzzer input. For example, if a parameter is an +Under the hood, Jazzer tries various ways of creating objects from the fuzzer input. For example, if a parameter is an interface or an abstract class, it will look for all concrete implementing classes on the classpath. -Jazzer can also create objects from classes that follow the [builder design pattern](https://www.baeldung.com/creational-design-patterns#builder) +Jazzer can also create objects from classes that follow the [builder design pattern](https://www.baeldung.com/creational-design-patterns#builder) or have a default constructor and use setters to set the fields. -Creating objects from fuzzer input can lead to many reported exceptions. -Jazzer addresses this issue by ignoring exceptions that the target method declares to throw. +Creating objects from fuzzer input can lead to many reported exceptions. +Jazzer addresses this issue by ignoring exceptions that the target method declares to throw. In addition to that, you can provide a list of exceptions to be ignored during fuzzing via the `--autofuzz_ignore` flag in the form of a comma-separated list. You can specify concrete exceptions (e.g., `java.lang.NullPointerException`), in which case also subclasses of these exception classes will be ignored, or glob patterns to ignore all exceptions in a specific package (e.g. `java.lang.*` or `com.company.**`). @@ -314,7 +301,7 @@ docker run -it cifuzz/jazzer-autofuzz \ --keep_going=1 ``` -#### +#### ### Reproducing a bug @@ -354,6 +341,9 @@ Jazzer has so far uncovered the following vulnerabilities and bugs: | Project | Bug | Status | CVE | found by | | ------- | -------- | ------ | --- | -------- | +| [OpenJDK](https://github.com/openjdk/jdk) | `OutOfMemoryError` via a small BMP image | [fixed](https://openjdk.java.net/groups/vulnerability/advisories/2022-01-18) | [CVE-2022-21360](https://nvd.nist.gov/vuln/detail/CVE-2022-21360) | [Code Intelligence](https://code-intelligence.com) | +| [OpenJDK](https://github.com/openjdk/jdk) | `OutOfMemoryError` via a small TIFF image | [fixed](https://openjdk.java.net/groups/vulnerability/advisories/2022-01-18) | [CVE-2022-21366](https://nvd.nist.gov/vuln/detail/CVE-2022-21366) | [Code Intelligence](https://code-intelligence.com) | +| [protocolbuffers/protobuf](https://github.com/protocolbuffers/protobuf) | Small protobuf messages can consume minutes of CPU time | [fixed](https://github.com/protocolbuffers/protobuf/security/advisories/GHSA-wrvw-hg22-4m67) | [CVE-2021-22569](https://nvd.nist.gov/vuln/detail/CVE-2021-22569) | [OSS-Fuzz](https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=39330) | | [jhy/jsoup](https://github.com/jhy/jsoup) | More than 19 Bugs found in HTML and XML parser | [fixed](https://github.com/jhy/jsoup/security/advisories/GHSA-m72m-mhq2-9p6c) | [CVE-2021-37714](https://nvd.nist.gov/vuln/detail/CVE-2021-37714) | [Code Intelligence](https://code-intelligence.com) | | [Apache/commons-compress](https://commons.apache.org/proper/commons-compress/) | Infinite loop when loading a crafted 7z | fixed | [CVE-2021-35515](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-35515) | [Code Intelligence](https://code-intelligence.com) | | [Apache/commons-compress](https://commons.apache.org/proper/commons-compress/) | `OutOfMemoryError` when loading a crafted 7z | fixed | [CVE-2021-35516](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-35516) | [Code Intelligence](https://code-intelligence.com) | @@ -376,7 +366,7 @@ Jazzer has so far uncovered the following vulnerabilities and bugs: | [google/re2j](https://github.com/google/re2j) | `NullPointerException` in `Pattern.compile` | [reported](https://github.com/google/re2j/issues/148) | | [@schirrmacher](https://github.com/schirrmacher) | | [google/gson](https://github.com/google/gson) | `ArrayIndexOutOfBounds` in `ParseString` | [fixed](https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=40838) | | [@DavidKorczynski](https://twitter.com/Davkorcz) | -As Jazzer is used to fuzz JVM projects in OSS-Fuzz, an additional list of bugs can be found [on the OSS-Fuzz issue tracker](https://bugs.chromium.org/p/oss-fuzz/issues/list?q=proj%3A%22json-sanitizer%22%20OR%20proj%3A%22fastjson2%22%20OR%20proj%3A%22jackson-core%22%20OR%20proj%3A%22jackson-dataformats-binary%22%20OR%20proj%3A%22jackson-dataformats-xml%22%20OR%20proj%3A%22apache-commons%22%20OR%20proj%3A%22jsoup%22&can=1). +As Jazzer is used to fuzz JVM projects in OSS-Fuzz, an additional list of bugs can be found [on the OSS-Fuzz issue tracker](https://bugs.chromium.org/p/oss-fuzz/issues/list?q=proj%3A%22json-sanitizer%22%20OR%20proj%3A%22fastjson2%22%20OR%20proj%3A%22jackson-core%22%20OR%20proj%3A%22jackson-dataformats-binary%22%20OR%20proj%3A%22jackson-dataformats-xml%22%20OR%20proj%3A%22apache-commons%22%20OR%20proj%3A%22jsoup%22%20OR%20proj%3A%22apache-commons-codec%22%20OR%20proj%3A%22apache-commons-io%22%20OR%20proj%3A%22apache-commons-jxpath%22%20OR%20proj%3A%22apache-commons-lang%22%20OR%20proj%3A%22httpcomponents-client%22%20OR%20proj%3A%22httpcomponents-core%22%20OR%20proj%3A%22tomcat%22%20OR%20proj%3A%22archaius-core%22%20OR%20proj%3A%22bc-java%22%20OR%20proj%3A%22gson%22%20OR%20proj%3A%22guava%22%20OR%20proj%3A%22guice%22%20OR%20proj%3A%22hdrhistogram%22%20OR%20proj%3A%22jackson-databind%22%20OR%20proj%3A%22javassist%22%20OR%20proj%3A%22jersey%22%20OR%20proj%3A%22jettison%22%20OR%20proj%3A%22joda-time%22%20OR%20proj%3A%22jul-to-slf4j%22%20OR%20proj%3A%22logback%22%20OR%20proj%3A%22servo-core%22%20OR%20proj%3A%22slf4j-api%22%20OR%20proj%3A%22snakeyaml%22%20OR%20proj%3A%22spring-boot-actuator%22%20OR%20proj%3A%22spring-boot%22%20OR%20proj%3A%22spring-framework%22%20OR%20proj%3A%22spring-security%22%20OR%20proj%3A%22stringtemplate4%22%20OR%20proj%3A%22woodstox%22%20OR%20proj%3A%22xmlpulll%22%20OR%20proj%3A%22xstream%22&can=1). If you find bugs with Jazzer, we would like to hear from you! Feel free to [open an issue](https://github.com/CodeIntelligenceTesting/jazzer/issues/new) or submit a pull request. @@ -392,7 +382,10 @@ to [its documentation](https://llvm.org/docs/LibFuzzer.html) for a detailed desc ### Passing JVM arguments -Arguments for the JVM started by Jazzer can be supplied via the `--jvm_args` argument. +When Jazzer is launched, it starts a JVM in which it executes the fuzz target. +Arguments for this JVM can be provided via the `JAVA_OPTS` environment variable. + +Alternatively, arguments can also be supplied via the `--jvm_args` argument. Multiple arguments are delimited by the classpath separator, which is `;` on Windows and `:` else. For example, to enable preview features as well as set a maximum heap size, add the following to the Jazzer invocation: @@ -403,6 +396,8 @@ For example, to enable preview features as well as set a maximum heap size, add --jvm_args=--enable-preview:-Xmx1000m ``` +Arguments specified with `--jvm_args` take precendence over those in `JAVA_OPTS`. + ### Coverage Instrumentation The Jazzer agent inserts coverage markers into the JVM bytecode during class loading. libFuzzer uses this information @@ -434,7 +429,7 @@ The particular instrumentation types to apply can be specified using the `--trac * `indir`: call through `Method#invoke` * `all`: shorthand to apply all available instrumentations (except `gep`) -Multiple instrumentation types can be combined with a colon. +Multiple instrumentation types can be combined with a colon (Linux, macOS) or a semicolon (Windows). ### Value Profile @@ -444,9 +439,6 @@ associated with the particular bytecode location and used to provide additional See [ExampleValueProfileFuzzer.java](https://github.com/CodeIntelligenceTesting/jazzer/tree/main/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java) for a fuzz target that would be very hard to fuzz without value profile. -As passing the bytecode location back to libFuzzer requires inline assembly and may thus not be fully portable, it can be disabled -via the flag `--nofake_pcs`. - ### Custom Hooks In order to obtain information about data passed into functions such as `String.equals` or `String.startsWith`, Jazzer @@ -465,6 +457,7 @@ for more details. To use the compiled method hooks they have to be available on the classpath provided by `--cp` and can then be loaded by providing the flag `--custom_hooks`, which takes a colon-separated list of names of classes to load hooks from. +If a hook is meant to be applied to a class in the Java standard library, it has to be loaded from a JAR file so that Jazzer can [add it to the bootstrap class loader search](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#appendToBootstrapClassLoaderSearch-java.util.jar.JarFile-). This list of custom hooks can alternatively be specified via the `Jazzer-Hook-Classes` attribute in the fuzz target JAR's manifest. @@ -475,6 +468,34 @@ With the flag `--keep_going=N` Jazzer continues fuzzing until `N` unique stack t Particular stack traces can also be ignored based on their `DEDUP_TOKEN` by passing a comma-separated list of tokens via `--ignore=<token_1>,<token2>`. +### Export coverage information + +The internally gathered JaCoCo coverage information can be exported in human-readable and JaCoCo execution data format +(`.exec`). These can help identify code areas that have not been covered by the fuzzer and thus may require more +comprehensive fuzz targets or a more extensive initial corpus to reach. + +The human-readable report contains coverage information, like branch and line coverage, on file level. It's useful to +get a quick overview about the overall coverage. The flag `--coverage_report=<file>` can be used to generate it. + +Similar to the JaCoCo `dump` command, the flag `--coverage_dump=<file>` specifies a coverage dump file, often called +`jacoco.exec`, that is generated after the fuzzing run. It contains a binary representation of the gathered coverage +data in the JaCoCo format. + +The JaCoCo `report` command can be used to generate reports based on this coverage dump. The JaCoCo CLI tools are +available on their [GitHub release page](https://github.com/jacoco/jacoco/releases) as `zip` file. The report tool is +located in the `lib` folder and can be used as described in the JaCoCo +[CLI documentation](https://www.eclemma.org/jacoco/trunk/doc/cli.html). For example the following command generates an +HTML report in the folder `report` containing all classes available in `classes.jar` and their coverage as captured in +the export `coverage.exec`. Source code to include in the report is searched for in `some/path/to/sources`. +After execution the `index.html` file in the output folder can be opened in a browser. +```shell +java -jar path/to/jacococli.jar report coverage.exec \ + --classfiles classes.jar \ + --sourcefiles some/path/to/sources \ + --html report \ + --name FuzzCoverageReport +``` + ## Advanced fuzz targets ### Fuzzing with Native Libraries @@ -505,14 +526,14 @@ pre-loading the mutator library: ```bash # Using Bazel: -LD_PRELOAD=libcustom_mutator.so ./bazelisk-linux-amd64 run //:jazzer -- <arguments> +LD_PRELOAD=libcustom_mutator.so bazel run //:jazzer -- <arguments> # Using the binary release: LD_PRELOAD=libcustom_mutator.so ./jazzer <arguments> ``` ## Credit -The following developers have contributed to Jazzer: +The following developers have contributed to Jazzer before its public release: [Sergej Dechand](https://github.com/serj), [Christian Hartlage](https://github.com/dende), diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index c07582b7..5589d57c 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -1,6 +1,6 @@ workspace(name = "jazzer") -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file", "http_jar") load("//:repositories.bzl", "jazzer_dependencies") jazzer_dependencies() @@ -32,28 +32,28 @@ http_archive( ], sha256 = "da607faed78c4cb5a5637ef74a36fdd2286f85ca5192222c4664efec2d529bb8", strip_prefix = "bazel-toolchain-0.6.3", - urls = ["https://github.com/grailbio/bazel-toolchain/archive/0.6.3.tar.gz"], + urls = ["https://github.com/grailbio/bazel-toolchain/archive/refs/tags/0.6.3.tar.gz"], ) http_archive( name = "googletest", - sha256 = "9dc9157a9a1551ec7a7e43daea9a694a0bb5fb8bec81235d8a1e6ef64c716dcb", - strip_prefix = "googletest-release-1.10.0", - url = "https://github.com/google/googletest/archive/release-1.10.0.tar.gz", + sha256 = "81964fe578e9bd7c94dfdb09c8e4d6e6759e19967e397dbea48d1c10e45d0df2", + strip_prefix = "googletest-release-1.12.1", + url = "https://github.com/google/googletest/archive/refs/tags/release-1.12.1.tar.gz", ) http_archive( name = "rules_foreign_cc", - sha256 = "8ab257584256e2c7eefa0c4e0794ae3be3e8f634f9ec0356da0a653dfed5da9a", - strip_prefix = "rules_foreign_cc-76198edc790de8e8514bddaa3895d1145fccd6aa", - url = "https://github.com/bazelbuild/rules_foreign_cc/archive/76198edc790de8e8514bddaa3895d1145fccd6aa.tar.gz", + sha256 = "6041f1374ff32ba711564374ad8e007aef77f71561a7ce784123b9b4b88614fc", + strip_prefix = "rules_foreign_cc-0.8.0", + url = "https://github.com/bazelbuild/rules_foreign_cc/archive/refs/tags/0.8.0.tar.gz", ) http_archive( name = "rules_jvm_external", - sha256 = "f36441aa876c4f6427bfb2d1f2d723b48e9d930b62662bf723ddfb8fc80f0140", - strip_prefix = "rules_jvm_external-4.1", - url = "https://github.com/bazelbuild/rules_jvm_external/archive/4.1.zip", + sha256 = "cd1a77b7b02e8e008439ca76fd34f5b07aecb8c752961f9640dea15e9e5ba1ca", + strip_prefix = "rules_jvm_external-4.2", + url = "https://github.com/bazelbuild/rules_jvm_external/archive/refs/tags/4.2.zip", ) http_archive( @@ -61,7 +61,13 @@ http_archive( build_file = "//third_party:libjpeg_turbo.BUILD", sha256 = "6a965adb02ad898b2ae48214244618fe342baea79db97157fdc70d8844ac6f09", strip_prefix = "libjpeg-turbo-2.0.90", - url = "https://github.com/libjpeg-turbo/libjpeg-turbo/archive/2.0.90.tar.gz", + url = "https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/2.0.90.tar.gz", +) + +http_jar( + name = "org_kohsuke_args4j_args4j", + sha256 = "91ddeaba0b24adce72291c618c00bbdce1c884755f6c4dba9c5c46e871c69ed6", + url = "https://repo1.maven.org/maven2/args4j/args4j/2.33/args4j-2.33.jar", ) load("@com_grail_bazel_toolchain//toolchain:deps.bzl", "bazel_toolchain_dependencies") @@ -97,6 +103,10 @@ maven_install( artifacts = MAVEN_ARTIFACTS, fail_if_repin_required = True, maven_install_json = "//:maven_install.json", + override_targets = { + "org.jetbrains.kotlin:kotlin-reflect": "@com_github_jetbrains_kotlin//:kotlin-reflect", + "org.jetbrains.kotlin:kotlin-stdlib": "@com_github_jetbrains_kotlin//:kotlin-stdlib", + }, repositories = [ "https://repo1.maven.org/maven2", ], @@ -106,3 +116,11 @@ maven_install( load("@maven//:defs.bzl", "pinned_maven_install") pinned_maven_install() + +http_file( + name = "genhtml", + downloaded_file_path = "genhtml", + executable = True, + sha256 = "4120cc9186a0687db218520a2d0dc9bae75d15faf41d87448b6b6c5140c19156", + urls = ["https://raw.githubusercontent.com/linux-test-project/lcov/6da8399c7a7a3370de2c69b16b092e945442ffcd/bin/genhtml"], +) diff --git a/agent/BUILD.bazel b/agent/BUILD.bazel index ddafc246..aedbe424 100644 --- a/agent/BUILD.bazel +++ b/agent/BUILD.bazel @@ -1,4 +1,6 @@ load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +load("//bazel:jar.bzl", "strip_jar") load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES") java_binary( @@ -6,19 +8,48 @@ java_binary( create_executable = False, deploy_manifest_lines = [ "Premain-Class: com.code_intelligence.jazzer.agent.Agent", - "Jazzer-Hook-Classes: {}".format(":".join(SANITIZER_CLASSES)), - ], + "Can-Retransform-Classes: true", + "Jazzer-Hook-Classes: ", + ] + [" {}:".format(c) for c in SANITIZER_CLASSES], runtime_deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/agent:agent_lib", + "//driver/src/main/java/com/code_intelligence/jazzer/driver", "//sanitizers", ], ) -jar_jar( +strip_jar( name = "jazzer_agent_deploy", + out = "jazzer_agent_deploy.jar", + jar = ":jazzer_agent_shaded_deploy", + paths_to_strip = [ + "module-info.class", + ], + visibility = ["//visibility:public"], +) + +jar_jar( + name = "jazzer_agent_shaded_deploy", input_jar = "jazzer_agent_unshaded_deploy.jar", rules = "agent_shade_rules", - visibility = ["//visibility:public"], +) + +sh_test( + name = "jazzer_agent_shading_test", + srcs = ["verify_shading.sh"], + args = [ + "$(rootpath :jazzer_agent_deploy)", + ], + data = [ + ":jazzer_agent_deploy", + "@local_jdk//:bin/jar", + ], + tags = [ + # Coverage instrumentation necessarily adds files to the jar that we + # wouldn't want to release and thus causes this test to fail. + "no-coverage", + ], + target_compatible_with = SKIP_ON_WINDOWS, ) java_binary( diff --git a/agent/agent_shade_rules b/agent/agent_shade_rules index b0e4d8c0..91073722 100644 --- a/agent/agent_shade_rules +++ b/agent/agent_shade_rules @@ -1,4 +1,6 @@ -rule kotlin.** com.code_intelligence.jazzer.third_party.kotlin.@1 -rule io.** com.code_intelligence.jazzer.third_party.io.@1 -rule nonapi.** com.code_intelligence.jazzer.third_party.nonapi.@1 -rule net.jodah.** com.code_intelligence.jazzer.third_party.net.jodah.@1 +rule com.github.** com.code_intelligence.jazzer.third_party.@0 +rule io.** com.code_intelligence.jazzer.third_party.@0 +rule kotlin.** com.code_intelligence.jazzer.third_party.@0 +rule net.** com.code_intelligence.jazzer.third_party.@0 +rule nonapi.** com.code_intelligence.jazzer.third_party.@0 +rule org.objectweb.** com.code_intelligence.jazzer.third_party.@0 diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel new file mode 100644 index 00000000..cf6acfbc --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/BUILD.bazel @@ -0,0 +1,6 @@ +java_plugin( + name = "JmhGeneratorAnnotationProcessor", + processor_class = "org.openjdk.jmh.generators.BenchmarkProcessor", + visibility = ["//agent/src/jmh/java:__subpackages__"], + deps = ["@maven//:org_openjdk_jmh_jmh_generator_annprocess"], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel new file mode 100644 index 00000000..fe68f903 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -0,0 +1,102 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library", "java_jni_library") +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//agent/src/jmh/java/com/code_intelligence/jazzer:jmh.bzl", "JMH_TEST_ARGS") + +java_binary( + name = "CoverageInstrumentationBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":coverage_instrumentation_benchmark", + ], +) + +java_test( + name = "CoverageInstrumentationBenchmarkTest", + args = JMH_TEST_ARGS, + jvm_flags = [ + "-XX:CompileCommand=print,*CoverageMap.recordCoverage", + ], + main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":coverage_instrumentation_benchmark", + ], +) + +java_library( + name = "coverage_instrumentation_benchmark", + srcs = ["CoverageInstrumentationBenchmark.java"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + runtime_deps = [ + "@maven//:com_mikesamuel_json_sanitizer", + ], + deps = [ + ":kotlin_strategies", + ":strategies", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) + +java_library( + name = "strategies", + srcs = [ + "DirectByteBuffer2CoverageMap.java", + "DirectByteBufferCoverageMap.java", + "Unsafe2CoverageMap.java", + "UnsafeBranchfreeCoverageMap.java", + "UnsafeCoverageMap.java", + "UnsafeSimpleIncrementCoverageMap.java", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@org_ow2_asm_asm//jar", + ], +) + +kt_jvm_library( + name = "kotlin_strategies", + srcs = ["DirectByteBufferStrategy.kt"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "@jazzer_jacoco//:jacoco_internal", + "@org_ow2_asm_asm//jar", + ], +) + +java_binary( + name = "EdgeCoverageInstrumentationBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":edge_coverage_instrumentation_benchmark", + ], +) + +java_test( + name = "EdgeCoverageInstrumentationBenchmarkTest", + args = JMH_TEST_ARGS, + main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":edge_coverage_instrumentation_benchmark", + ], +) + +java_jni_library( + name = "edge_coverage_instrumentation_benchmark", + srcs = [ + "EdgeCoverageInstrumentation.java", + "EdgeCoverageTarget.java", + ], + native_libs = ["//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", + "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:patch_test_utils", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java new file mode 100644 index 00000000..f388c4cc --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationBenchmark.java @@ -0,0 +1,178 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +/** + * This benchmark compares the throughput of a typical fuzz target when instrumented with different + * edge coverage instrumentation strategies and coverage map implementations. + * + * The benchmark currently uses the OWASP json-sanitizer as its target, which has the following + * desirable properties for a benchmark: + * - It is a reasonably sized project that does not consist of many different classes. + * - It is very heavy on computation with a high density of branching. + * - It is entirely CPU bound with no IO and does not call expensive methods from the standard + * library. + * With these properties, results obtained from this benchmark should provide reasonable lower + * bounds on the relative slowdown introduced by the various approaches to instrumentations. + */ +@State(Scope.Benchmark) +public class CoverageInstrumentationBenchmark { + private static final String TARGET_CLASSNAME = "com.google.json.JsonSanitizer"; + private static final String TARGET_PACKAGE = + TARGET_CLASSNAME.substring(0, TARGET_CLASSNAME.lastIndexOf('.')); + private static final String TARGET_METHOD = "sanitize"; + private static final MethodType TARGET_TYPE = MethodType.methodType(String.class, String.class); + + // This is part of the benchmark's state and not a constant to prevent constant folding. + String TARGET_ARG = + "{\"foo\":1123987,\"bar\":[true, false],\"baz\":{\"foo\":\"132ä3\",\"bar\":1.123e-005}}"; + + MethodHandle uninstrumented_sanitize; + MethodHandle local_DirectByteBuffer_NeverZero_sanitize; + MethodHandle staticMethod_DirectByteBuffer_NeverZero_sanitize; + MethodHandle staticMethod_DirectByteBuffer2_NeverZero_sanitize; + MethodHandle staticMethod_Unsafe_NeverZero_sanitize; + MethodHandle staticMethod_Unsafe_NeverZero2_sanitize; + MethodHandle staticMethod_Unsafe_NeverZeroBranchfree_sanitize; + MethodHandle staticMethod_Unsafe_SimpleIncrement_sanitize; + + public static MethodHandle handleForTargetMethod(ClassLoader classLoader) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + Class<?> targetClass = classLoader.loadClass(TARGET_CLASSNAME); + return MethodHandles.lookup().findStatic(targetClass, TARGET_METHOD, TARGET_TYPE); + } + + public static MethodHandle instrumentWithStrategy( + EdgeCoverageStrategy strategy, Class<?> coverageMapClass) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + if (strategy == null) { + // Do not instrument the code by using the benchmark class' ClassLoader. + return handleForTargetMethod(CoverageInstrumentationBenchmark.class.getClassLoader()); + } + // It's fine to reuse a single instrumentor here as we don't want to know which class received + // how many counters. + Instrumentor instrumentor = new EdgeCoverageInstrumentor(strategy, coverageMapClass, 0); + return handleForTargetMethod(new InstrumentingClassLoader(instrumentor, TARGET_PACKAGE)); + } + + @Setup + public void instrumentWithStrategies() + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { + uninstrumented_sanitize = instrumentWithStrategy(null, null); + local_DirectByteBuffer_NeverZero_sanitize = instrumentWithStrategy( + DirectByteBufferStrategy.INSTANCE, DirectByteBufferCoverageMap.class); + staticMethod_DirectByteBuffer_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBufferCoverageMap.class); + staticMethod_DirectByteBuffer2_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), DirectByteBuffer2CoverageMap.class); + staticMethod_Unsafe_NeverZero_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeCoverageMap.class); + staticMethod_Unsafe_NeverZero2_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), Unsafe2CoverageMap.class); + staticMethod_Unsafe_SimpleIncrement_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeSimpleIncrementCoverageMap.class); + staticMethod_Unsafe_NeverZeroBranchfree_sanitize = + instrumentWithStrategy(new StaticMethodStrategy(), UnsafeBranchfreeCoverageMap.class); + } + + @Benchmark + public String uninstrumented() throws Throwable { + return (String) uninstrumented_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String local_DirectByteBuffer_NeverZero() throws Throwable { + return (String) local_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_DirectByteBuffer_NeverZero() throws Throwable { + return (String) staticMethod_DirectByteBuffer_NeverZero_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_DirectByteBuffer2_NeverZero() throws Throwable { + return (String) staticMethod_DirectByteBuffer2_NeverZero_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_NeverZero() throws Throwable { + return (String) staticMethod_Unsafe_NeverZero_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_NeverZero2() throws Throwable { + return (String) staticMethod_Unsafe_NeverZero2_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_SimpleIncrement() throws Throwable { + return (String) staticMethod_Unsafe_SimpleIncrement_sanitize.invokeExact(TARGET_ARG); + } + + @Benchmark + public String staticMethod_Unsafe_NeverZeroBranchfree() throws Throwable { + return (String) staticMethod_Unsafe_NeverZeroBranchfree_sanitize.invokeExact(TARGET_ARG); + } +} + +class InstrumentingClassLoader extends ClassLoader { + private final Instrumentor instrumentor; + private final String classNamePrefix; + + InstrumentingClassLoader(Instrumentor instrumentor, String packageToInstrument) { + super(InstrumentingClassLoader.class.getClassLoader()); + this.instrumentor = instrumentor; + this.classNamePrefix = packageToInstrument + "."; + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + if (!name.startsWith(classNamePrefix)) { + return super.loadClass(name); + } + try (InputStream stream = super.getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + throw new ClassNotFoundException(String.format("Failed to find class file for %s", name)); + } + byte[] bytecode = readAllBytes(stream); + byte[] instrumentedBytecode = instrumentor.instrument(bytecode); + return defineClass(name, instrumentedBytecode, 0, instrumentedBytecode.length); + } catch (IOException e) { + throw new ClassNotFoundException(String.format("Failed to read class file for %s", name), e); + } + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[64 * 104 * 1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java new file mode 100644 index 00000000..c57babb5 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBuffer2CoverageMap.java @@ -0,0 +1,32 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.nio.ByteBuffer; + +public final class DirectByteBuffer2CoverageMap { + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final int NUM_COUNTERS = 4096; + public static final ByteBuffer counters = ByteBuffer.allocateDirect(NUM_COUNTERS); + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final byte counter = counters.get(id); + counters.put(id, (byte) (counter == -1 ? 1 : counter + 1)); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java new file mode 100644 index 00000000..e5e66abb --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferCoverageMap.java @@ -0,0 +1,36 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.nio.ByteBuffer; + +public final class DirectByteBufferCoverageMap { + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final int NUM_COUNTERS = 4096; + public static final ByteBuffer counters = ByteBuffer.allocateDirect(NUM_COUNTERS); + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final byte counter = counters.get(id); + if (counter == -1) { + counters.put(id, (byte) 1); + } else { + counters.put(id, (byte) (counter + 1)); + } + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt new file mode 100644 index 00000000..49090184 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/DirectByteBufferStrategy.kt @@ -0,0 +1,81 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +object DirectByteBufferStrategy : EdgeCoverageStrategy { + + override fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) { + mv.apply { + visitVarInsn(Opcodes.ALOAD, variable) + // Stack: counters + push(edgeId) + // Stack: counters | edgeId + visitInsn(Opcodes.DUP2) + // Stack: counters | edgeId | counters | edgeId + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "get", "(I)B", false) + // Increment the counter, but ensure that it never stays at 0 after an overflow by incrementing it again in + // that case. + // This approach performs better than saturating the counter at 255 (see Section 3.3 of + // https://www.usenix.org/system/files/woot20-paper-fioraldi.pdf) + // Stack: counters | edgeId | counter (sign-extended to int) + push(0xff) + // Stack: counters | edgeId | counter (sign-extended to int) | 0x000000ff + visitInsn(Opcodes.IAND) + // Stack: counters | edgeId | counter (zero-extended to int) + push(1) + // Stack: counters | edgeId | counter | 1 + visitInsn(Opcodes.IADD) + // Stack: counters | edgeId | counter + 1 + visitInsn(Opcodes.DUP) + // Stack: counters | edgeId | counter + 1 | counter + 1 + push(8) + // Stack: counters | edgeId | counter + 1 | counter + 1 | 8 (maxStack: +5) + visitInsn(Opcodes.ISHR) + // Stack: counters | edgeId | counter + 1 | 1 if the increment overflowed to 0, 0 otherwise + visitInsn(Opcodes.IADD) + // Stack: counters | edgeId | counter + 2 if the increment overflowed, counter + 1 otherwise + visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "put", "(IB)Ljava/nio/ByteBuffer;", false) + // Stack: counters + visitInsn(Opcodes.POP) + } + } + + override val instrumentControlFlowEdgeStackSize = 5 + + override val localVariableType get() = "java/nio/ByteBuffer" + + override fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) { + mv.apply { + visitFieldInsn( + Opcodes.GETSTATIC, + coverageMapInternalClassName, + "counters", + "Ljava/nio/ByteBuffer;", + ) + // Stack: counters (maxStack: 1) + visitVarInsn(Opcodes.ASTORE, variable) + } + } + + override val loadLocalVariableStackSize = 1 +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java new file mode 100644 index 00000000..e2eeadd3 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentation.java @@ -0,0 +1,66 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import static com.code_intelligence.jazzer.instrumentor.PatchTestUtils.*; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; + +import com.code_intelligence.jazzer.runtime.CoverageMap; +import java.lang.invoke.*; +import java.nio.file.Files; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; + +@Warmup(iterations = 10, time = 3) +@Measurement(iterations = 10, time = 3) +@Fork(value = 3) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +@SuppressWarnings("unused") +public class EdgeCoverageInstrumentation { + private MethodHandle exampleMethod; + + @Setup + public void setupInstrumentation() throws Throwable { + String outDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + if (outDir == null || outDir.isEmpty()) { + outDir = + Files.createTempDirectory(EdgeCoverageInstrumentation.class.getSimpleName()).toString(); + } + + byte[] originalBytecode = classToBytecode(EdgeCoverageTarget.class); + dumpBytecode(outDir, EdgeCoverageTarget.class.getName(), originalBytecode); + + byte[] patchedBytecode = applyInstrumentation(originalBytecode); + dumpBytecode(outDir, EdgeCoverageTarget.class.getName() + ".patched", patchedBytecode); + + Class<?> patchedClass = bytecodeToClass(EdgeCoverageTarget.class.getName(), patchedBytecode); + Object obj = lookup().findConstructor(patchedClass, methodType(void.class)).invoke(); + exampleMethod = lookup().bind(obj, "exampleMethod", methodType(List.class)); + } + + private byte[] applyInstrumentation(byte[] bytecode) { + return new EdgeCoverageInstrumentor(new StaticMethodStrategy(), CoverageMap.class, 0) + .instrument(bytecode); + } + + @Benchmark + public Object benchmarkInstrumentedMethodCall() throws Throwable { + return exampleMethod.invoke(); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java new file mode 100644 index 00000000..57eb8807 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageTarget.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.instrumentor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +public class EdgeCoverageTarget { + private final Random rnd = new Random(); + + @SuppressWarnings("unused") + public List<Integer> exampleMethod() { + ArrayList<Integer> rnds = new ArrayList<>(); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + rnds.add(rnd.nextInt()); + int i = rnd.nextInt() + rnd.nextInt(); + if (i > 0 && i < Integer.MAX_VALUE / 2) { + i--; + } else { + i++; + } + rnds.add(i); + return rnds.stream().map(n -> n + 1).collect(Collectors.toList()); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java new file mode 100644 index 00000000..030d9a95 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/Unsafe2CoverageMap.java @@ -0,0 +1,55 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class Unsafe2CoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + UNSAFE.putByte(address, (byte) (counter == -1 ? 1 : counter + 1)); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java new file mode 100644 index 00000000..3694b95f --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeBranchfreeCoverageMap.java @@ -0,0 +1,55 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeBranchfreeCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final int incrementedCounter = UNSAFE.getByte(address) + 1; + UNSAFE.putByte(address, (byte) (incrementedCounter ^ (incrementedCounter >>> 8))); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java new file mode 100644 index 00000000..cf73928d --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeCoverageMap.java @@ -0,0 +1,59 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + if (counter == -1) { + UNSAFE.putByte(address, (byte) 1); + } else { + UNSAFE.putByte(address, (byte) (counter + 1)); + } + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java new file mode 100644 index 00000000..60fb8c8d --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor/UnsafeSimpleIncrementCoverageMap.java @@ -0,0 +1,54 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeSimpleIncrementCoverageMap { + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + unsafe = (Unsafe) f.get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + System.exit(1); + // Not reached. + unsafe = null; + } + UNSAFE = unsafe; + } + + // The current target, JsonSanitizer, uses less than 2048 coverage counters. + private static final long NUM_COUNTERS = 4096; + private static final long countersAddress = UNSAFE.allocateMemory(NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, NUM_COUNTERS, (byte) 0); + } + + public static void enlargeIfNeeded(int nextId) { + // Statically sized counters buffer. + } + + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + UNSAFE.putByte(address, (byte) (UNSAFE.getByte(address) + 1)); + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl index 1463c602..5391a46b 100755..100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl @@ -1,5 +1,4 @@ -#!/usr/bin/env sh -# Copyright 2021 Code Intelligence GmbH +# Copyright 2022 Code Intelligence GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -e -bazel build //agent/src/main/java/com/code_intelligence/jazzer/generated:java_no_throw_methods_list -cp bazel-bin/agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat.generated agent/src/main/java/com/code_intelligence/jazzer/generated/java_no_throw_methods_list.dat +JMH_TEST_ARGS = [ + # Fail fast on any exceptions produced by benchmarks. + "-foe true", + "-wf 0", + "-f 1", + "-wi 0", + "-i 1", + "-r 1s", + "-w 1s", +] diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..96fd8e1f --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,50 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("//agent/src/jmh/java/com/code_intelligence/jazzer:jmh.bzl", "JMH_TEST_ARGS") + +java_binary( + name = "FuzzerCallbacksBenchmark", + main_class = "org.openjdk.jmh.Main", + runtime_deps = [ + ":fuzzer_callbacks_benchmark", + ], +) + +java_test( + name = "FuzzerCallbacksBenchmarkTest", + args = JMH_TEST_ARGS, + main_class = "org.openjdk.jmh.Main", + # Directly invoke JMH's main without using a testrunner. + use_testrunner = False, + runtime_deps = [ + ":fuzzer_callbacks_benchmark", + ], +) + +java_library( + name = "fuzzer_callbacks_benchmark", + srcs = ["FuzzerCallbacksBenchmark.java"], + plugins = ["//agent/src/jmh/java/com/code_intelligence/jazzer:JmhGeneratorAnnotationProcessor"], + deps = [ + ":fuzzer_callbacks", + "@maven//:org_openjdk_jmh_jmh_core", + ], +) + +java_jni_library( + name = "fuzzer_callbacks", + srcs = [ + "FuzzerCallbacks.java", + "FuzzerCallbacksOptimizedCritical.java", + "FuzzerCallbacksOptimizedNonCritical.java", + # Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + # "FuzzerCallbacksPanama.java", + "FuzzerCallbacksWithPc.java", + ], + javacopts = [ + # Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + # "--add-modules", + # "jdk.incubator.foreign", + ], + native_libs = ["//agent/src/jmh/native/com/code_intelligence/jazzer/runtime:fuzzer_callbacks"], + visibility = ["//agent/src/jmh/native/com/code_intelligence/jazzer/runtime:__pkg__"], +) diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java new file mode 100644 index 00000000..6e8343ce --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacks.java @@ -0,0 +1,29 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.github.fmeum.rules_jni.RulesJni; + +public final class FuzzerCallbacks { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacks.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + static native void traceStrstr(String s1, String s2, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java new file mode 100644 index 00000000..b55a9936 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksBenchmark.java @@ -0,0 +1,219 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 3) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class FuzzerCallbacksBenchmark { + @State(Scope.Benchmark) + public static class TraceCmpIntState { + int arg1 = 0xCAFECAFE; + int arg2 = 0xFEEDFEED; + int pc = 0x12345678; + } + + @Benchmark + public void traceCmpInt(TraceCmpIntState state) { + FuzzerCallbacks.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + @Benchmark + public void traceCmpIntWithPc(TraceCmpIntState state) { + FuzzerCallbacksWithPc.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceCmpIntOptimizedCritical(TraceCmpIntState state) { + FuzzerCallbacksOptimizedCritical.traceCmpInt(state.arg1, state.arg2, state.pc); + } + + // Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + // @Benchmark + // @Fork(jvmArgsAppend = {"--enable-native-access=ALL-UNNAMED", "--add-modules", + // "jdk.incubator.foreign"}) + // public void + // traceCmpIntPanama(TraceCmpIntState state) throws Throwable { + // FuzzerCallbacksPanama.traceCmpInt(state.arg1, state.arg2, state.pc); + // } + + @State(Scope.Benchmark) + public static class TraceSwitchState { + @Param({"5", "10"}) int numCases; + + long val; + long[] cases; + int pc = 0x12345678; + + @Setup + public void setup() { + cases = new long[2 + numCases]; + Random random = ThreadLocalRandom.current(); + Arrays.setAll(cases, i -> { + switch (i) { + case 0: + return numCases; + case 1: + return 32; + default: + return random.nextInt(); + } + }); + Arrays.sort(cases, 2, cases.length); + val = random.nextInt(); + } + } + + @Benchmark + public void traceSwitch(TraceSwitchState state) { + FuzzerCallbacks.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + public void traceSwitchWithPc(TraceSwitchState state) { + FuzzerCallbacksWithPc.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceSwitchOptimizedCritical(TraceSwitchState state) { + FuzzerCallbacksOptimizedCritical.traceSwitch(state.val, state.cases, state.pc); + } + + @Benchmark + public void traceSwitchOptimizedNonCritical(TraceSwitchState state) { + FuzzerCallbacksOptimizedNonCritical.traceSwitch(state.val, state.cases, state.pc); + } + + // Uncomment to benchmark Project Panama-backed implementation (requires JDK 16+). + // @Benchmark + // @Fork(jvmArgsAppend = {"--enable-native-access=ALL-UNNAMED", "--add-modules", + // "jdk.incubator.foreign"}) + // public void + // traceCmpSwitchPanama(TraceSwitchState state) throws Throwable { + // FuzzerCallbacksPanama.traceCmpSwitch(state.val, state.cases, state.pc); + // } + + @State(Scope.Benchmark) + public static class TraceMemcmpState { + @Param({"10", "100", "1000"}) int length; + + byte[] array1; + byte[] array2; + int pc = 0x12345678; + + @Setup + public void setup() { + array1 = new byte[length]; + array2 = new byte[length]; + + Random random = ThreadLocalRandom.current(); + random.nextBytes(array1); + random.nextBytes(array2); + // Make the arrays agree unil the midpoint to benchmark the "average" + // case of an interesting memcmp. + System.arraycopy(array1, 0, array2, 0, length / 2); + } + } + + @Benchmark + public void traceMemcmp(TraceMemcmpState state) { + FuzzerCallbacks.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceMemcmpOptimizedCritical(TraceMemcmpState state) { + FuzzerCallbacksOptimizedCritical.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @Benchmark + public void traceMemcmpOptimizedNonCritical(TraceMemcmpState state) { + FuzzerCallbacksOptimizedNonCritical.traceMemcmp(state.array1, state.array2, 1, state.pc); + } + + @State(Scope.Benchmark) + public static class TraceStrstrState { + @Param({"10", "100", "1000"}) int length; + @Param({"true", "false"}) boolean asciiOnly; + + String haystack; + String needle; + int pc = 0x12345678; + + @Setup + public void setup() { + haystack = randomString(length, asciiOnly); + needle = randomString(length, asciiOnly); + } + + private String randomString(int length, boolean asciiOnly) { + String asciiString = + ThreadLocalRandom.current() + .ints('a', 'z' + 1) + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + if (asciiOnly) { + return asciiString; + } + // Force String to be non-Latin-1 to preclude compact string optimization. + return "\uFFFD" + asciiString.substring(1); + } + } + + @Benchmark + public void traceStrstr(TraceStrstrState state) { + FuzzerCallbacks.traceStrstr(state.haystack, state.needle, state.pc); + } + + @Benchmark + public void traceStrstrOptimizedNonCritical(TraceStrstrState state) { + FuzzerCallbacksOptimizedNonCritical.traceStrstr(state.haystack, state.needle, state.pc); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-XX:+CriticalJNINatives"}) + public void traceStrstrOptimizedJavaCritical(TraceStrstrState state) + throws UnsupportedEncodingException { + FuzzerCallbacksOptimizedCritical.traceStrstrJava(state.haystack, state.needle, state.pc); + } + + @Benchmark + public void traceStrstrOptimizedJavaNonCritical(TraceStrstrState state) + throws UnsupportedEncodingException { + FuzzerCallbacksOptimizedNonCritical.traceStrstrJava(state.haystack, state.needle, state.pc); + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java new file mode 100644 index 00000000..1c09e9ad --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedCritical.java @@ -0,0 +1,46 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.github.fmeum.rules_jni.RulesJni; +import java.io.UnsupportedEncodingException; + +/** + * Optimized implementations of the libFuzzer callbacks that do rely on the deprecated + * CriticalJNINatives feature. Methods with `Java` in their name implement some parts in Java. + */ +public final class FuzzerCallbacksOptimizedCritical { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksOptimizedCritical.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + static void traceStrstrJava(String haystack, String needle, int pc) + throws UnsupportedEncodingException { + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is + // more likely to insert literal null bytes, having both the fuzzer input and the reported + // string comparisons be CESU8 should perform even better than the current implementation using + // modified UTF-8. + traceStrstrInternal(needle.substring(0, Math.min(needle.length(), 64)).getBytes("CESU8"), pc); + } + + private static native void traceStrstrInternal(byte[] needle, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java new file mode 100644 index 00000000..25fad3bf --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksOptimizedNonCritical.java @@ -0,0 +1,46 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.github.fmeum.rules_jni.RulesJni; +import java.io.UnsupportedEncodingException; + +/** + * Optimized implementations of the libFuzzer callbacks that do not rely on the deprecated + * CriticalJNINatives feature. Methods with `Java` in their name implement some parts in Java. + */ +public final class FuzzerCallbacksOptimizedNonCritical { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksOptimizedNonCritical.class); + } + + static native void traceSwitch(long val, long[] cases, int pc); + + static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); + + static native void traceStrstr(String s1, String s2, int pc); + + static void traceStrstrJava(String haystack, String needle, int pc) + throws UnsupportedEncodingException { + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is + // more likely to insert literal null bytes, having both the fuzzer input and the reported + // string comparisons be CESU8 should perform even better than the current implementation using + // modified UTF-8. + traceStrstrInternal(needle.substring(0, Math.min(needle.length(), 64)).getBytes("CESU8"), pc); + } + + private static native void traceStrstrInternal(byte[] needle, int pc); +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java new file mode 100644 index 00000000..ce3d6290 --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksPanama.java @@ -0,0 +1,59 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.github.fmeum.rules_jni.RulesJni; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import jdk.incubator.foreign.CLinker; +import jdk.incubator.foreign.FunctionDescriptor; +import jdk.incubator.foreign.MemoryAddress; +import jdk.incubator.foreign.MemoryLayout; +import jdk.incubator.foreign.MemorySegment; +import jdk.incubator.foreign.ResourceScope; +import jdk.incubator.foreign.SymbolLookup; + +/** + * Pure-Java implementation of the fuzzer callbacks backed by Project Panama (requires JDK 16+). + * To include the implementation in the benchmark on a supported JDK, uncomment the relevant lines + * in BUILD.bazel. + */ +public class FuzzerCallbacksPanama { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacks.class); + } + + private static final MethodHandle traceCmp4 = CLinker.getInstance().downcallHandle( + SymbolLookup.loaderLookup().lookup("__sanitizer_cov_trace_cmp4").get(), + MethodType.methodType(void.class, int.class, int.class), + FunctionDescriptor.ofVoid(CLinker.C_INT, CLinker.C_INT)); + private static final MethodHandle traceSwitch = CLinker.getInstance().downcallHandle( + SymbolLookup.loaderLookup().lookup("__sanitizer_cov_trace_switch").get(), + MethodType.methodType(void.class, long.class, MemoryAddress.class), + FunctionDescriptor.ofVoid(CLinker.C_LONG, CLinker.C_POINTER)); + + static void traceCmpInt(int arg1, int arg2, int pc) throws Throwable { + traceCmp4.invokeExact(arg1, arg2); + } + + static void traceCmpSwitch(long val, long[] cases, int pc) throws Throwable { + try (ResourceScope scope = ResourceScope.newConfinedScope()) { + MemorySegment nativeCopy = MemorySegment.allocateNative( + MemoryLayout.sequenceLayout(cases.length, CLinker.C_LONG), scope); + nativeCopy.copyFrom(MemorySegment.ofArray(cases)); + traceSwitch.invokeExact(val, nativeCopy.address()); + } + } +} diff --git a/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java new file mode 100644 index 00000000..21f416cf --- /dev/null +++ b/agent/src/jmh/java/com/code_intelligence/jazzer/runtime/FuzzerCallbacksWithPc.java @@ -0,0 +1,31 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.github.fmeum.rules_jni.RulesJni; + +/** + * Unoptimized implementation of the libFuzzer callbacks that use the trampoline construction to + * inject fake PCs. + */ +public final class FuzzerCallbacksWithPc { + static { + RulesJni.loadLibrary("fuzzer_callbacks", FuzzerCallbacksWithPc.class); + } + + static native void traceCmpInt(int arg1, int arg2, int pc); + + static native void traceSwitch(long val, long[] cases, int pc); +} diff --git a/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..33a03036 --- /dev/null +++ b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,12 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") + +cc_jni_library( + name = "fuzzer_callbacks", + srcs = ["fuzzer_callbacks.cpp"], + visibility = ["//agent/src/jmh/java/com/code_intelligence/jazzer/runtime:__pkg__"], + deps = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/runtime:fuzzer_callbacks.hdrs", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:sanitizer_hooks_with_pc", + "@jazzer_libfuzzer//:libfuzzer_no_main", + ], +) diff --git a/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp new file mode 100644 index 00000000..2562db1f --- /dev/null +++ b/agent/src/jmh/native/com/code_intelligence/jazzer/runtime/fuzzer_callbacks.cpp @@ -0,0 +1,213 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <jni.h> + +#include <cstddef> +#include <cstdint> + +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacks.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical.h" +#include "com_code_intelligence_jazzer_runtime_FuzzerCallbacksWithPc.h" +#include "driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_hooks_with_pc.h" + +extern "C" { +void __sanitizer_weak_hook_compare_bytes(void *caller_pc, const void *s1, + const void *s2, std::size_t n1, + std::size_t n2, int result); +void __sanitizer_weak_hook_strstr(void *caller_pc, const char *s1, + const char *s2, const char *result); +void __sanitizer_weak_hook_memmem(void *caller_pc, const void *b1, + std::size_t n1, const void *s2, + std::size_t n2, void *result); +void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2); +void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2); + +void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases); + +void __sanitizer_cov_trace_div4(uint32_t val); +void __sanitizer_cov_trace_div8(uint64_t val); + +void __sanitizer_cov_trace_gep(uintptr_t idx); +} + +inline __attribute__((always_inline)) void *idToPc(jint id) { + return reinterpret_cast<void *>(static_cast<uintptr_t>(id)); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksWithPc_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceCmpInt( + jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4(value1, value2); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + jlong *case_values = + env->GetLongArrayElements(libfuzzer_case_values, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); + env->ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + auto *case_values = static_cast<jlong *>( + env->GetPrimitiveArrayCritical(libfuzzer_case_values, nullptr)); + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); + env->ReleasePrimitiveArrayCritical(libfuzzer_case_values, case_values, + JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksWithPc_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + jlong *case_values = + env->GetLongArrayElements(libfuzzer_case_values, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_cov_trace_switch_with_pc( + idToPc(id), switch_value, reinterpret_cast<uint64_t *>(case_values)); + env->ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceSwitch( + env, cls, switch_value, libfuzzer_case_values, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceSwitch( + jlong switch_value, jint case_values_length, jlong *case_values, jint id) { + __sanitizer_cov_trace_switch(switch_value, + reinterpret_cast<uint64_t *>(case_values)); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + jbyte *b1_native = env->GetByteArrayElements(b1, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jbyte *b2_native = env->GetByteArrayElements(b2, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jint b1_length = env->GetArrayLength(b1); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + jint b2_length = env->GetArrayLength(b2); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, + b1_length, b2_length, result); + env->ReleaseByteArrayElements(b1, b1_native, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + env->ReleaseByteArrayElements(b2, b2_native, JNI_ABORT); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + auto *b1_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b1, nullptr)); + auto *b2_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b2, nullptr)); + jint b1_length = env->GetArrayLength(b1); + jint b2_length = env->GetArrayLength(b2); + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, + b1_length, b2_length, result); + env->ReleasePrimitiveArrayCritical(b1, b1_native, JNI_ABORT); + env->ReleasePrimitiveArrayCritical(b2, b2_native, JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceMemcmp( + env, cls, b1, b2, result, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceMemcmp( + jint b1_length, jbyte *b1, jint b2_length, jbyte *b2, jint result, + jint id) { + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1, b2, b1_length, b2_length, + result); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacks_traceStrstr( + JNIEnv *env, jclass cls, jstring s1, jstring s2, jint id) { + const char *s1_native = env->GetStringUTFChars(s1, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + const char *s2_native = env->GetStringUTFChars(s2, nullptr); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + // libFuzzer currently ignores the result, which allows us to simply pass a + // valid but arbitrary pointer here instead of performing an actual strstr + // operation. + __sanitizer_weak_hook_strstr(idToPc(id), s1_native, s2_native, s1_native); + env->ReleaseStringUTFChars(s1, s1_native); + if (env->ExceptionCheck()) env->ExceptionDescribe(); + env->ReleaseStringUTFChars(s2, s2_native); + if (env->ExceptionCheck()) env->ExceptionDescribe(); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstr( + JNIEnv *env, jclass cls, jstring s1, jstring s2, jint id) { + const char *s2_native = env->GetStringUTFChars(s2, nullptr); + __sanitizer_weak_hook_strstr(idToPc(id), nullptr, s2_native, s2_native); + env->ReleaseStringUTFChars(s2, s2_native); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstrInternal( + JNIEnv *env, jclass cls, jbyteArray needle, jint id) { + auto *needle_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(needle, nullptr)); + jint needle_length = env->GetArrayLength(needle); + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle_native, + needle_length, nullptr); + env->ReleasePrimitiveArrayCritical(needle, needle_native, JNI_ABORT); +} + +void Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceStrstrInternal( + JNIEnv *env, jclass cls, jbyteArray needle, jint id) { + Java_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedNonCritical_traceStrstrInternal( + env, cls, needle, id); +} + +extern "C" JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_FuzzerCallbacksOptimizedCritical_traceStrstrInternal( + jint needle_length, jbyte *needle, jint id) { + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle, needle_length, + nullptr); +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt index 33d02263..f9b026f1 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt @@ -16,38 +16,35 @@ package com.code_intelligence.jazzer.agent +import com.code_intelligence.jazzer.driver.Opt import com.code_intelligence.jazzer.instrumentor.CoverageRecorder +import com.code_intelligence.jazzer.instrumentor.Hooks import com.code_intelligence.jazzer.instrumentor.InstrumentationType -import com.code_intelligence.jazzer.instrumentor.loadHooks -import com.code_intelligence.jazzer.runtime.ManifestUtils +import com.code_intelligence.jazzer.runtime.NativeLibHooks +import com.code_intelligence.jazzer.runtime.TraceCmpHooks +import com.code_intelligence.jazzer.runtime.TraceDivHooks +import com.code_intelligence.jazzer.runtime.TraceIndirHooks import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.ManifestUtils import java.io.File import java.lang.instrument.Instrumentation +import java.net.URI import java.nio.file.Paths import java.util.jar.JarFile import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.exists import kotlin.io.path.isDirectory -val KNOWN_ARGUMENTS = listOf( - "instrumentation_includes", - "instrumentation_excludes", - "custom_hook_includes", - "custom_hook_excludes", - "trace", - "custom_hooks", - "id_sync_file", - "dump_classes_dir", -) - private object AgentJarFinder { - private val agentJarPath = AgentJarFinder::class.java.protectionDomain?.codeSource?.location?.toURI() - val agentJarFile = agentJarPath?.let { JarFile(File(it)) } + val agentJarFile = jarUriForClass(AgentJarFinder::class.java)?.let { JarFile(File(it)) } } -private val argumentDelimiter = if (System.getProperty("os.name").startsWith("Windows")) ";" else ":" +fun jarUriForClass(clazz: Class<*>): URI? { + return clazz.protectionDomain?.codeSource?.location?.toURI() +} @OptIn(ExperimentalPathApi::class) +@Suppress("UNUSED_PARAMETER") fun premain(agentArgs: String?, instrumentation: Instrumentation) { // Add the agent jar (i.e., the jar out of which we are currently executing) to the search path of the bootstrap // class loader to ensure that instrumented classes can find the CoverageMap class regardless of which ClassLoader @@ -57,37 +54,25 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } else { println("WARN: Failed to add agent JAR to bootstrap class loader search path") } - val argumentMap = (agentArgs ?: "") - .split(',') - .mapNotNull { - val splitArg = it.split('=', limit = 2) - when { - splitArg.size != 2 -> { - if (splitArg[0].isNotEmpty()) - println("WARN: Ignoring argument ${splitArg[0]} without value") - null - } - splitArg[0] !in KNOWN_ARGUMENTS -> { - println("WARN: Ignoring unknown argument ${splitArg[0]}") - null - } - else -> splitArg[0] to splitArg[1].split(argumentDelimiter) - } - }.toMap() - val manifestCustomHookNames = ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES).flatMap { - it.split(':') + + val manifestCustomHookNames = + ManifestUtils.combineManifestValues(ManifestUtils.HOOK_CLASSES).flatMap { + it.split(':') + }.filter { it.isNotBlank() } + val allCustomHookNames = (manifestCustomHookNames + Opt.customHooks).toSet() + val disabledCustomHookNames = Opt.disabledHooks.toSet() + val customHookNames = allCustomHookNames - disabledCustomHookNames + val disabledCustomHooksToPrint = allCustomHookNames - customHookNames.toSet() + if (disabledCustomHooksToPrint.isNotEmpty()) { + println("INFO: Not using the following disabled hooks: ${disabledCustomHooksToPrint.joinToString(", ")}") } - val customHookNames = manifestCustomHookNames + (argumentMap["custom_hooks"] ?: emptyList()) - val classNameGlobber = ClassNameGlobber( - argumentMap["instrumentation_includes"] ?: emptyList(), - (argumentMap["instrumentation_excludes"] ?: emptyList()) + customHookNames - ) + + val classNameGlobber = ClassNameGlobber(Opt.instrumentationIncludes, Opt.instrumentationExcludes + customHookNames) CoverageRecorder.classNameGlobber = classNameGlobber - val dependencyClassNameGlobber = ClassNameGlobber( - argumentMap["custom_hook_includes"] ?: emptyList(), - (argumentMap["custom_hook_excludes"] ?: emptyList()) + customHookNames - ) - val instrumentationTypes = (argumentMap["trace"] ?: listOf("all")).flatMap { + val customHookClassNameGlobber = ClassNameGlobber(Opt.customHookIncludes, Opt.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 = (Opt.trace.takeIf { it.isNotEmpty() } ?: listOf("all")).flatMap { when (it) { "cmp" -> setOf(InstrumentationType.CMP) "cov" -> setOf(InstrumentationType.COV) @@ -106,13 +91,13 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } } }.toSet() - val idSyncFile = argumentMap["id_sync_file"]?.let { - Paths.get(it.single()).also { path -> + val idSyncFile = Opt.idSyncFile.takeUnless { it.isEmpty() }?.let { + Paths.get(it).also { path -> println("INFO: Synchronizing coverage IDs in ${path.toAbsolutePath()}") } } - val dumpClassesDir = argumentMap["dump_classes_dir"]?.let { - Paths.get(it.single()).toAbsolutePath().also { path -> + val dumpClassesDir = Opt.dumpClassesDir.takeUnless { it.isEmpty() }?.let { + Paths.get(it).toAbsolutePath().also { path -> if (path.exists() && path.isDirectory()) { println("INFO: Dumping instrumented classes into $path") } else { @@ -120,41 +105,67 @@ fun premain(agentArgs: String?, instrumentation: Instrumentation) { } } } + val includedHookNames = instrumentationTypes + .mapNotNull { type -> + when (type) { + InstrumentationType.CMP -> TraceCmpHooks::class.java.name + InstrumentationType.DIV -> TraceDivHooks::class.java.name + InstrumentationType.INDIR -> TraceIndirHooks::class.java.name + InstrumentationType.NATIVE -> NativeLibHooks::class.java.name + else -> null + } + } + val coverageIdSynchronizer = if (idSyncFile != null) + FileSyncCoverageIdStrategy(idSyncFile) + else + MemSyncCoverageIdStrategy() + + val (includedHooks, customHooks) = Hooks.loadHooks(includedHookNames.toSet(), customHookNames.toSet()) + // If we don't append the JARs containing the custom hooks to the bootstrap class loader, + // third-party hooks not contained in the agent JAR will not be able to instrument Java standard + // library classes. These classes are loaded by the bootstrap / system class loader and would + // not be considered when resolving references to hook methods, leading to NoClassDefFoundError + // being thrown. + customHooks.hookClasses + .mapNotNull { jarUriForClass(it) } + .toSet() + .map { JarFile(File(it)) } + .forEach { instrumentation.appendToBootstrapClassLoaderSearch(it) } + val runtimeInstrumentor = RuntimeInstrumentor( instrumentation, classNameGlobber, - dependencyClassNameGlobber, + customHookClassNameGlobber, instrumentationTypes, - idSyncFile, + includedHooks.hooks, + customHooks.hooks, + customHooks.additionalHookClassNameGlobber, + coverageIdSynchronizer, dumpClassesDir, ) - instrumentation.apply { - addTransformer(runtimeInstrumentor) - } - val relevantClassesLoadedBeforeCustomHooks = instrumentation.allLoadedClasses - .map { it.name } - .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) } - .toSet() - val customHooks = customHookNames.toSet().flatMap { hookClassName -> - try { - loadHooks(Class.forName(hookClassName)).also { - println("INFO: Loaded ${it.size} hooks from $hookClassName") - } - } catch (_: ClassNotFoundException) { - println("WARN: Failed to load hooks from $hookClassName") - emptySet() + // These classes are e.g. dependencies of the RuntimeInstrumentor or hooks and thus were loaded + // before the instrumentor was ready. Since we haven't enabled it yet, they can safely be + // "retransformed": They haven't been transformed yet. + val classesToRetransform = instrumentation.allLoadedClasses + .filter { + classNameGlobber.includes(it.name) || + customHookClassNameGlobber.includes(it.name) || + customHooks.additionalHookClassNameGlobber.includes(it.name) } - } - val relevantClassesLoadedAfterCustomHooks = instrumentation.allLoadedClasses - .map { it.name } - .filter { classNameGlobber.includes(it) || dependencyClassNameGlobber.includes(it) } - .toSet() - val nonHookClassesLoadedByHooks = relevantClassesLoadedAfterCustomHooks - relevantClassesLoadedBeforeCustomHooks - if (nonHookClassesLoadedByHooks.isNotEmpty()) { - println("WARN: Hooks were not applied to the following classes as they are dependencies of hooks:") - println("WARN: ${nonHookClassesLoadedByHooks.joinToString()}") - } + .filter { + instrumentation.isModifiableClass(it) + } + .toTypedArray() + + instrumentation.addTransformer(runtimeInstrumentor, true) - runtimeInstrumentor.registerCustomHooks(customHooks) + if (classesToRetransform.isNotEmpty()) { + if (instrumentation.isRetransformClassesSupported) { + instrumentation.retransformClasses(*classesToRetransform) + } else { + println("WARN: Instrumentation was not applied to the following classes as they are dependencies of hooks:") + println("WARN: ${classesToRetransform.joinToString()}") + } + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel index 2d5eec5c..db6ae264 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/BUILD.bazel @@ -11,5 +11,6 @@ kt_jvm_library( deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:opt", ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt index fd2a1e7c..5d1d28e3 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/CoverageIdStrategy.kt @@ -14,7 +14,8 @@ package com.code_intelligence.jazzer.agent -import java.nio.ByteBuffer +import com.code_intelligence.jazzer.utils.append +import com.code_intelligence.jazzer.utils.readFully import java.nio.channels.FileChannel import java.nio.channels.FileLock import java.nio.file.Path @@ -24,59 +25,42 @@ import java.util.UUID /** * Indicates a fatal failure to generate synchronized coverage IDs. */ -internal class CoverageIdException(cause: Throwable? = null) : +class CoverageIdException(cause: Throwable? = null) : RuntimeException("Failed to synchronize coverage IDs", cause) +/** + * [CoverageIdStrategy] provides an abstraction to switch between context specific coverage ID generation. + * + * Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp + * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. + * This precludes us from generating them simply as hashes of class names. + */ interface CoverageIdStrategy { - /** - * Obtain the first coverage ID to be used for the class [className]. - * The caller *must* also call [commitIdCount] once it has instrumented that class, even if instrumentation fails. - */ - @Throws(CoverageIdException::class) - fun obtainFirstId(className: String): Int /** - * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId]. - * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0. + * [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 commitIdCount(idCount: Int) + fun withIdForClass(className: String, block: (Int) -> Int) } /** - * An unsynchronized strategy for coverage ID generation that simply increments a global counter. + * A memory synced strategy for coverage ID generation. + * + * This strategy uses a synchronized block to guard access to a global edge ID counter. + * Even though concurrent fuzzing is not fully supported this strategy enables consistent coverage + * IDs in case of concurrent class loading. + * + * It only prevents races within one VM instance. */ -internal class TrivialCoverageIdStrategy : CoverageIdStrategy { +class MemSyncCoverageIdStrategy : CoverageIdStrategy { private var nextEdgeId = 0 - override fun obtainFirstId(className: String) = nextEdgeId - - override fun commitIdCount(idCount: Int) { - nextEdgeId += idCount - } -} - -/** - * Reads the [FileChannel] to the end as a UTF-8 string. - */ -private fun FileChannel.readFully(): String { - check(size() <= Int.MAX_VALUE) - val buffer = ByteBuffer.allocate(size().toInt()) - while (buffer.hasRemaining()) { - when (read(buffer)) { - 0 -> throw IllegalStateException("No bytes read") - -1 -> break - } + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + nextEdgeId += block(nextEdgeId) } - return String(buffer.array()) -} - -/** - * Appends [string] to the end of the [FileChannel]. - */ -private fun FileChannel.append(string: String) { - position(size()) - write(ByteBuffer.wrap(string.toByteArray())) } /** @@ -84,19 +68,30 @@ private fun FileChannel.append(string: String) { * specified [idSyncFile]. * This class takes care of synchronizing the access to the file between multiple processes as long as the general * contract of [CoverageIdStrategy] is followed. - * - * Rationale: Coverage (i.e., edge) IDs differ from other kinds of IDs, such as those generated for call sites or cmp - * instructions, in that they should be consecutive, collision-free, and lie in a known, small range. This precludes us - * from generating them simply as hashes of class names and explains why go through the arduous process of synchronizing - * them across multiple agents. */ -internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy { - val uuid: UUID = UUID.randomUUID() - var idFileLock: FileLock? = null +class FileSyncCoverageIdStrategy(private val idSyncFile: Path) : CoverageIdStrategy { + private val uuid: UUID = UUID.randomUUID() + private var idFileLock: FileLock? = null + + private var cachedFirstId: Int? = null + private var cachedClassName: String? = null + private var cachedIdCount: Int? = null - var cachedFirstId: Int? = null - var cachedClassName: String? = null - var cachedIdCount: Int? = null + /** + * This method is synchronized to prevent concurrent access to the internal file lock which would result in + * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId] + * is always committed back again to the sync file by [commitIdCount]. + */ + @Synchronized + override fun withIdForClass(className: String, block: (Int) -> Int) { + var actualNumEdgeIds = 0 + try { + val firstId = obtainFirstId(className) + actualNumEdgeIds = block(firstId) + } finally { + commitIdCount(actualNumEdgeIds) + } + } /** * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID. @@ -108,7 +103,7 @@ internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : Co * In this case, the lock on the file is returned immediately and the extracted first coverage ID is returned to * the caller. The caller is still expected to call [commitIdCount] so that desynchronization can be detected. */ - override fun obtainFirstId(className: String): Int { + private fun obtainFirstId(className: String): Int { try { check(idFileLock == null) { "Already holding a lock on the ID file" } val localIdFile = FileChannel.open( @@ -170,7 +165,11 @@ internal class SynchronizedCoverageIdStrategy(private val idSyncFile: Path) : Co } } - override fun commitIdCount(idCount: Int) { + /** + * Records the number of coverage IDs used to instrument the class specified in a previous call to [obtainFirstId]. + * If instrumenting the class should fail, this function must still be called. In this case, [idCount] is set to 0. + */ + private fun commitIdCount(idCount: Int) { val localIdFileLock = idFileLock try { check(cachedClassName != null) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt index e2283aa2..fe2efd54 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt @@ -18,11 +18,6 @@ import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor import com.code_intelligence.jazzer.instrumentor.CoverageRecorder import com.code_intelligence.jazzer.instrumentor.Hook import com.code_intelligence.jazzer.instrumentor.InstrumentationType -import com.code_intelligence.jazzer.instrumentor.loadHooks -import com.code_intelligence.jazzer.runtime.NativeLibHooks -import com.code_intelligence.jazzer.runtime.TraceCmpHooks -import com.code_intelligence.jazzer.runtime.TraceDivHooks -import com.code_intelligence.jazzer.runtime.TraceIndirHooks import com.code_intelligence.jazzer.utils.ClassNameGlobber import java.lang.instrument.ClassFileTransformer import java.lang.instrument.Instrumentation @@ -32,37 +27,25 @@ import kotlin.math.roundToInt import kotlin.system.exitProcess import kotlin.time.measureTimedValue -internal class RuntimeInstrumentor( +class RuntimeInstrumentor( private val instrumentation: Instrumentation, - private val classesToInstrument: ClassNameGlobber, - private val dependencyClassesToInstrument: ClassNameGlobber, + private val classesToFullyInstrument: ClassNameGlobber, + private val classesToHookInstrument: ClassNameGlobber, private val instrumentationTypes: Set<InstrumentationType>, - idSyncFile: Path?, + private val includedHooks: List<Hook>, + private val customHooks: List<Hook>, + // Dedicated name globber for additional classes to hook stated in hook annotations is needed due to + // existing include and exclude pattern of classesToHookInstrument. All classes are included in hook + // instrumentation except the ones from default excludes, like JDK and Kotlin classes. But additional + // classes to hook, based on annotations, are allowed to reference normally ignored ones, like JDK + // and Kotlin internals. + // FIXME: Adding an additional class to hook will apply _all_ hooks to it and not only the one it's + // defined in. At some point we might want to track the list of classes per custom hook rather than globally. + private val additionalClassesToHookInstrument: ClassNameGlobber, + private val coverageIdSynchronizer: CoverageIdStrategy, private val dumpClassesDir: Path?, ) : ClassFileTransformer { - private val coverageIdSynchronizer = if (idSyncFile != null) - SynchronizedCoverageIdStrategy(idSyncFile) - else - TrivialCoverageIdStrategy() - - private val includedHooks = instrumentationTypes - .mapNotNull { type -> - when (type) { - InstrumentationType.CMP -> TraceCmpHooks::class.java - InstrumentationType.DIV -> TraceDivHooks::class.java - InstrumentationType.INDIR -> TraceIndirHooks::class.java - InstrumentationType.NATIVE -> NativeLibHooks::class.java - else -> null - } - } - .flatMap { loadHooks(it) } - private val customHooks = emptyList<Hook>().toMutableList() - - fun registerCustomHooks(hooks: List<Hook>) { - customHooks.addAll(hooks) - } - @OptIn(kotlin.time.ExperimentalTime::class) override fun transform( loader: ClassLoader?, @@ -86,15 +69,20 @@ internal class RuntimeInstrumentor( }.also { instrumentedByteCode -> // Only dump classes that were instrumented. if (instrumentedByteCode != null && dumpClassesDir != null) { - val relativePath = "$internalClassName.class" - val absolutePath = dumpClassesDir.resolve(relativePath) - val dumpFile = absolutePath.toFile() - dumpFile.parentFile.mkdirs() - dumpFile.writeBytes(instrumentedByteCode) + dumpToClassFile(internalClassName, instrumentedByteCode) + dumpToClassFile(internalClassName, classfileBuffer, basenameSuffix = ".original") } } } + private fun dumpToClassFile(internalClassName: String, bytecode: ByteArray, basenameSuffix: String = "") { + val relativePath = "$internalClassName$basenameSuffix.class" + val absolutePath = dumpClassesDir!!.resolve(relativePath) + val dumpFile = absolutePath.toFile() + dumpFile.parentFile.mkdirs() + dumpFile.writeBytes(bytecode) + } + override fun transform( module: Module?, loader: ClassLoader?, @@ -103,33 +91,42 @@ internal class RuntimeInstrumentor( protectionDomain: ProtectionDomain?, classfileBuffer: ByteArray ): ByteArray? { - if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) { - // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the - // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the - // injected bytecode might throw NoClassDefFoundError. - // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html - if (!instrumentation.isModifiableModule(module)) { - val prettyClassName = internalClassName.replace('/', '.') - println("WARN: Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping") - return null + return try { + if (module != null && !module.canRead(RuntimeInstrumentor::class.java.module)) { + // Make all other modules read our (unnamed) module, which allows them to access the classes needed by the + // instrumentations, e.g. CoverageMap. If a module can't be modified, it should not be instrumented as the + // injected bytecode might throw NoClassDefFoundError. + // https://mail.openjdk.java.net/pipermail/jigsaw-dev/2021-May/014663.html + if (!instrumentation.isModifiableModule(module)) { + val prettyClassName = internalClassName.replace('/', '.') + println("WARN: Failed to instrument $prettyClassName in unmodifiable module ${module.name}, skipping") + return null + } + instrumentation.redefineModule( + module, + /* extraReads */ setOf(RuntimeInstrumentor::class.java.module), + emptyMap(), + emptyMap(), + emptySet(), + emptyMap() + ) } - instrumentation.redefineModule( - module, - /* extraReads */ setOf(RuntimeInstrumentor::class.java.module), - emptyMap(), - emptyMap(), - emptySet(), - emptyMap() - ) + transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer) + } catch (t: Throwable) { + // Throwables raised from transform are silently dropped, making it extremely hard to detect instrumentation + // failures. The docs advise to use a top-level try-catch. + // https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/ClassFileTransformer.html + t.printStackTrace() + throw t } - return transform(loader, internalClassName, classBeingRedefined, protectionDomain, classfileBuffer) } @OptIn(kotlin.time.ExperimentalTime::class) fun transformInternal(internalClassName: String, classfileBuffer: ByteArray): ByteArray? { val fullInstrumentation = when { - classesToInstrument.includes(internalClassName) -> true - dependencyClassesToInstrument.includes(internalClassName) -> false + classesToFullyInstrument.includes(internalClassName) -> true + classesToHookInstrument.includes(internalClassName) -> false + additionalClassesToHookInstrument.includes(internalClassName) -> false else -> return null } val prettyClassName = internalClassName.replace('/', '.') @@ -165,14 +162,16 @@ internal class RuntimeInstrumentor( // trigger the GEP callbacks for ByteBuffer. traceDataFlow(instrumentationTypes) hooks(includedHooks + customHooks) - val firstId = coverageIdSynchronizer.obtainFirstId(internalClassName) - var actualNumEdgeIds = 0 - try { - actualNumEdgeIds = coverage(firstId) - } finally { - coverageIdSynchronizer.commitIdCount(actualNumEdgeIds) + coverageIdSynchronizer.withIdForClass(internalClassName) { firstId -> + coverage(firstId).also { actualNumEdgeIds -> + CoverageRecorder.recordInstrumentedClass( + internalClassName, + bytecode, + firstId, + actualNumEdgeIds + ) + } } - CoverageRecorder.recordInstrumentedClass(internalClassName, bytecode, firstId, firstId + actualNumEdgeIds) } else { hooks(customHooks) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel index e573e757..b26bb846 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -23,6 +23,7 @@ java_library( "Jazzer.java", "MethodHook.java", "MethodHooks.java", + "//agent/src/main/java/jaz", ], visibility = ["//visibility:public"], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java index 4402a7f3..fbde853b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueCritical.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a critical severity security issue rather than * a normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java index 4d323e56..05837b0e 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueHigh.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a high severity security issue rather than a * normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java index f0de4ce7..be7c8c8f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/FuzzerSecurityIssueMedium.java @@ -17,7 +17,7 @@ package com.code_intelligence.jazzer.api; /** * Thrown to indicate that a fuzz target has detected a medium severity security issue rather than a * normal bug. - * + * <p> * There is only a semantical but no functional difference between throwing exceptions of this type * or any other. However, automated fuzzing platforms can use the extra information to handle the * detected issues appropriately. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java index 1c564a78..8ed4337f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/HookType.java @@ -17,6 +17,7 @@ package com.code_intelligence.jazzer.api; /** * The type of a {@link MethodHook}. */ +// Note: The order of entries is important and is used during instrumentation. public enum HookType { BEFORE, REPLACE, diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java index e45f7600..97adf578 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java @@ -18,32 +18,69 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.InvocationTargetException; +import java.security.SecureRandom; /** * Helper class with static methods that interact with Jazzer at runtime. */ final public class Jazzer { - private static Class<?> jazzerInternal = null; - - private static MethodHandle traceStrcmp = null; - private static MethodHandle traceStrstr = null; - private static MethodHandle traceMemcmp = null; - - private static MethodHandle consume = null; - private static MethodHandle autofuzzFunction1 = null; - private static MethodHandle autofuzzFunction2 = null; - private static MethodHandle autofuzzFunction3 = null; - private static MethodHandle autofuzzFunction4 = null; - private static MethodHandle autofuzzFunction5 = null; - private static MethodHandle autofuzzConsumer1 = null; - private static MethodHandle autofuzzConsumer2 = null; - private static MethodHandle autofuzzConsumer3 = null; - private static MethodHandle autofuzzConsumer4 = null; - private static MethodHandle autofuzzConsumer5 = null; + /** + * A 32-bit random number that hooks can use to make pseudo-random choices + * between multiple possible mutations they could guide the fuzzer towards. + * Hooks <b>must not</b> base the decision whether or not to report a finding + * on this number as this will make findings non-reproducible. + * <p> + * This is the same number that libFuzzer uses as a seed internally, which + * makes it possible to deterministically reproduce a previous fuzzing run by + * supplying the seed value printed by libFuzzer as the value of the + * {@code -seed}. + */ + public static final int SEED = getLibFuzzerSeed(); + + private static final Class<?> JAZZER_INTERNAL; + + private static final MethodHandle ON_FUZZ_TARGET_READY; + + private static final MethodHandle TRACE_STRCMP; + private static final MethodHandle TRACE_STRSTR; + private static final MethodHandle TRACE_MEMCMP; + private static final MethodHandle TRACE_PC_INDIR; + + private static final MethodHandle CONSUME; + private static final MethodHandle AUTOFUZZ_FUNCTION_1; + private static final MethodHandle AUTOFUZZ_FUNCTION_2; + private static final MethodHandle AUTOFUZZ_FUNCTION_3; + private static final MethodHandle AUTOFUZZ_FUNCTION_4; + private static final MethodHandle AUTOFUZZ_FUNCTION_5; + private static final MethodHandle AUTOFUZZ_CONSUMER_1; + private static final MethodHandle AUTOFUZZ_CONSUMER_2; + private static final MethodHandle AUTOFUZZ_CONSUMER_3; + private static final MethodHandle AUTOFUZZ_CONSUMER_4; + private static final MethodHandle AUTOFUZZ_CONSUMER_5; static { + Class<?> jazzerInternal = null; + MethodHandle onFuzzTargetReady = null; + MethodHandle traceStrcmp = null; + MethodHandle traceStrstr = null; + MethodHandle traceMemcmp = null; + MethodHandle tracePcIndir = null; + MethodHandle consume = null; + MethodHandle autofuzzFunction1 = null; + MethodHandle autofuzzFunction2 = null; + MethodHandle autofuzzFunction3 = null; + MethodHandle autofuzzFunction4 = null; + MethodHandle autofuzzFunction5 = null; + MethodHandle autofuzzConsumer1 = null; + MethodHandle autofuzzConsumer2 = null; + MethodHandle autofuzzConsumer3 = null; + MethodHandle autofuzzConsumer4 = null; + MethodHandle autofuzzConsumer5 = null; try { jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal"); + MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class); + onFuzzTargetReady = MethodHandles.publicLookup().findStatic( + jazzerInternal, "registerOnFuzzTargetReadyCallback", onFuzzTargetReadyType); Class<?> traceDataFlowNativeCallbacks = Class.forName("com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks"); @@ -60,6 +97,9 @@ final public class Jazzer { MethodType.methodType(void.class, byte[].class, byte[].class, int.class, int.class); traceMemcmp = MethodHandles.publicLookup().findStatic( traceDataFlowNativeCallbacks, "traceMemcmp", traceMemcmpType); + MethodType tracePcIndirType = MethodType.methodType(void.class, int.class, int.class); + tracePcIndir = MethodHandles.publicLookup().findStatic( + traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType); Class<?> metaClass = Class.forName("com.code_intelligence.jazzer.autofuzz.Meta"); MethodType consumeType = @@ -96,6 +136,23 @@ final public class Jazzer { e.printStackTrace(); System.exit(1); } + JAZZER_INTERNAL = jazzerInternal; + ON_FUZZ_TARGET_READY = onFuzzTargetReady; + TRACE_STRCMP = traceStrcmp; + TRACE_STRSTR = traceStrstr; + TRACE_MEMCMP = traceMemcmp; + TRACE_PC_INDIR = tracePcIndir; + CONSUME = consume; + AUTOFUZZ_FUNCTION_1 = autofuzzFunction1; + AUTOFUZZ_FUNCTION_2 = autofuzzFunction2; + AUTOFUZZ_FUNCTION_3 = autofuzzFunction3; + AUTOFUZZ_FUNCTION_4 = autofuzzFunction4; + AUTOFUZZ_FUNCTION_5 = autofuzzFunction5; + AUTOFUZZ_CONSUMER_1 = autofuzzConsumer1; + AUTOFUZZ_CONSUMER_2 = autofuzzConsumer2; + AUTOFUZZ_CONSUMER_3 = autofuzzConsumer3; + AUTOFUZZ_CONSUMER_4 = autofuzzConsumer4; + AUTOFUZZ_CONSUMER_5 = autofuzzConsumer5; } private Jazzer() {} @@ -103,7 +160,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -120,7 +177,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) { try { - return (R) autofuzzFunction1.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_1.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -133,7 +190,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -150,7 +207,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) { try { - return (R) autofuzzFunction2.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_2.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -163,7 +220,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -180,7 +237,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) { try { - return (R) autofuzzFunction3.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_3.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -193,7 +250,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -211,7 +268,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, R> R autofuzz( FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) { try { - return (R) autofuzzFunction4.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_4.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -224,7 +281,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -242,7 +299,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, T5, R> R autofuzz( FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) { try { - return (R) autofuzzFunction5.invoke(data, func); + return (R) AUTOFUZZ_FUNCTION_5.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -255,7 +312,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -269,7 +326,7 @@ final public class Jazzer { */ public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) { try { - autofuzzConsumer1.invoke(data, func); + AUTOFUZZ_CONSUMER_1.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -280,7 +337,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -294,7 +351,7 @@ final public class Jazzer { */ public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) { try { - autofuzzConsumer2.invoke(data, func); + AUTOFUZZ_CONSUMER_2.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -305,7 +362,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -319,7 +376,7 @@ final public class Jazzer { */ public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) { try { - autofuzzConsumer3.invoke(data, func); + AUTOFUZZ_CONSUMER_3.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -330,7 +387,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -345,7 +402,7 @@ final public class Jazzer { public static <T1, T2, T3, T4> void autofuzz( FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) { try { - autofuzzConsumer4.invoke(data, func); + AUTOFUZZ_CONSUMER_4.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -356,7 +413,7 @@ final public class Jazzer { /** * Attempts to invoke {@code func} with arguments created automatically from the fuzzer input * using only public methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to execute {@code func} in * meaningful ways for a number of reasons. * @@ -371,7 +428,7 @@ final public class Jazzer { public static <T1, T2, T3, T4, T5> void autofuzz( FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) { try { - autofuzzConsumer5.invoke(data, func); + AUTOFUZZ_CONSUMER_5.invoke(data, func); } catch (AutofuzzInvocationException e) { rethrowUnchecked(e.getCause()); } catch (Throwable t) { @@ -382,7 +439,7 @@ final public class Jazzer { /** * Attempts to construct an instance of {@code type} from the fuzzer input using only public * methods available on the classpath. - * + * <p> * <b>Note:</b> This function is inherently heuristic and may fail to return meaningful values for * a variety of reasons. * @@ -394,7 +451,7 @@ final public class Jazzer { @SuppressWarnings("unchecked") public static <T> T consume(FuzzedDataProvider data, Class<T> type) { try { - return (T) consume.invokeExact(data, type); + return (T) CONSUME.invokeExact(data, type); } catch (AutofuzzConstructionException ignored) { return null; } catch (Throwable t) { @@ -407,7 +464,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code * target}. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code current} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * achieve equality. @@ -417,8 +474,11 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsEquality(String current, String target, int id) { + if (TRACE_STRCMP == null) { + return; + } try { - traceStrcmp.invokeExact(current, target, 1, id); + TRACE_STRCMP.invokeExact(current, target, 1, id); } catch (Throwable e) { e.printStackTrace(); } @@ -427,7 +487,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code current} equal to {@code * target}. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code current} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * achieve equality. @@ -437,8 +497,11 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsEquality(byte[] current, byte[] target, int id) { + if (TRACE_MEMCMP == null) { + return; + } try { - traceMemcmp.invokeExact(current, target, 1, id); + TRACE_MEMCMP.invokeExact(current, target, 1, id); } catch (Throwable e) { e.printStackTrace(); } @@ -447,7 +510,7 @@ final public class Jazzer { /** * Instructs the fuzzer to guide its mutations towards making {@code haystack} contain {@code * needle} as a substring. - * + * <p> * If the relation between the raw fuzzer input and the value of {@code haystack} is relatively * complex, running the fuzzer with the argument {@code -use_value_profile=1} may be necessary to * satisfy the substring check. @@ -458,28 +521,81 @@ final public class Jazzer { * @param id a (probabilistically) unique identifier for this particular compare hint */ public static void guideTowardsContainment(String haystack, String needle, int id) { + if (TRACE_STRSTR == null) { + return; + } try { - traceStrstr.invokeExact(haystack, needle, id); + TRACE_STRSTR.invokeExact(haystack, needle, id); } catch (Throwable e) { e.printStackTrace(); } } /** - * Make Jazzer report the provided {@link Throwable} as a finding. + * Instructs the fuzzer to attain as many possible values for the absolute value of {@code state} + * as possible. + * <p> + * Call this function from a fuzz target or a hook to help the fuzzer track partial progress + * (e.g. by passing the length of a common prefix of two lists that should become equal) or + * explore different values of state that is not directly related to code coverage (see the + * MazeFuzzer example). + * <p> + * <b>Note:</b> This hint only takes effect if the fuzzer is run with the argument + * {@code -use_value_profile=1}. * + * @param state a numeric encoding of a state that should be varied by the fuzzer + * @param id a (probabilistically) unique identifier for this particular state hint + */ + public static void exploreState(byte state, int id) { + if (TRACE_PC_INDIR == null) { + return; + } + // We only use the lower 7 bits of state, which allows for 128 different state values tracked + // per id. The particular amount of 7 bits of state is also used in libFuzzer's + // TracePC::HandleCmp: + // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390 + // This value should be large enough for most use cases (e.g. tracking the length of a prefix in + // a comparison) while being small enough that the bitmap isn't filled up too quickly + // (65536 bits/ 128 bits per id = 512 ids). + + // We use tracePcIndir as a way to set a bit in libFuzzer's value profile bitmap. In + // TracePC::HandleCallerCallee, which is what this function ultimately calls through to, the + // lower 12 bits of each argument are combined into a 24-bit index into the bitmap, which is + // then reduced modulo a 16-bit prime. To keep the modulo bias small, we should fill as many + // of the relevant bits as possible. However, there are the following restrictions: + // 1. Since we use the return address trampoline to set the caller address indirectly, its + // upper 3 bits are fixed, which leaves a total of 21 variable bits on x86_64. + // 2. On arm64 macOS, where every instruction is aligned to 4 bytes, the lower 2 bits of the + // caller address will always be zero, further reducing the number of variable bits in the + // caller parameter to 7. + // https://github.com/llvm/llvm-project/blob/c12d49c4e286fa108d4d69f1c6d2b8d691993ffd/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L121 + // Even taking these restrictions into consideration, we pass state in the lowest bits of the + // caller address, which is used to form the lowest bits of the bitmap index. This should result + // in the best caching behavior as state is expected to change quickly in consecutive runs and + // in this way all its bitmap entries would be located close to each other in memory. + int lowerBits = (state & 0x7f) | (id << 7); + int upperBits = id >>> 5; + try { + TRACE_PC_INDIR.invokeExact(upperBits, lowerBits); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Make Jazzer report the provided {@link Throwable} as a finding. + * <p> * <b>Note:</b> This method must only be called from a method hook. In a * fuzz target, simply throw an exception to trigger a finding. * @param finding the finding that Jazzer should report */ public static void reportFindingFromHook(Throwable finding) { try { - jazzerInternal.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding); + JAZZER_INTERNAL.getMethod("reportFindingFromHook", Throwable.class).invoke(null, finding); } catch (NullPointerException | IllegalAccessException | NoSuchMethodException e) { - // We can only reach this point if the runtime is not in the classpath, but it must be if - // hooks work and this function should only be called from them. - System.err.println("ERROR: Jazzer.reportFindingFromHook must be called from a method hook"); - System.exit(1); + // We can only reach this point if the runtime is not on the classpath, e.g. in case of a + // reproducer. Just throw the finding. + rethrowUnchecked(finding); } catch (InvocationTargetException e) { // reportFindingFromHook throws a HardToCatchThrowable, which will bubble up wrapped in an // InvocationTargetException that should not be stopped here. @@ -491,6 +607,34 @@ final public class Jazzer { } } + /** + * Register a callback to be executed right before the fuzz target is executed for the first time. + * <p> + * This can be used to disable hooks until after Jazzer has been fully initializing, e.g. to + * prevent Jazzer internals from triggering hooks on Java standard library classes. + * + * @param callback the callback to execute + */ + public static void onFuzzTargetReady(Runnable callback) { + try { + ON_FUZZ_TARGET_READY.invokeExact(callback); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + private static int getLibFuzzerSeed() { + // The Jazzer driver sets this property based on the value of libFuzzer's -seed command-line + // option, which allows for fully reproducible fuzzing runs if set. If not running in the + // context of the driver, fall back to a random number instead. + String rawSeed = System.getProperty("jazzer.seed"); + if (rawSeed == null) { + return new SecureRandom().nextInt(); + } + // If jazzer.seed is set, we expect it to be a valid integer. + return Integer.parseUnsignedInt(rawSeed); + } + // Rethrows a (possibly checked) exception while avoiding a throws declaration. @SuppressWarnings("unchecked") private static <T extends Throwable> void rethrowUnchecked(Throwable t) throws T { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java index 0d17a4a0..3a1c5f39 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/api/MethodHook.java @@ -23,11 +23,12 @@ import java.lang.annotation.Target; import java.lang.invoke.MethodType; /** - * Registers this method as a hook that should run after the method - * specified by the annotation parameters has returned. + * Registers the annotated method as a hook that should run before, instead or + * after the method specified by the annotation parameters. * <p> - * This method will be called after every call to the target method and has - * access to its return value. The target method is specified by + * Depending on {@link #type()} this method will be called after, instead or + * before every call to the target method and has + * access to its parameters and return value. The target method is specified by * {@link #targetClassName()} and {@link #targetMethod()}. In case of an * overloaded method, {@link #targetMethodDescriptor()} can be used to restrict * the application of the hook to a particular overload. @@ -87,7 +88,7 @@ import java.lang.invoke.MethodType; * <p> * Return value: the value that should take the role of the value the target * method would have returned - * + * <p> * <dt><span class="strong">{@link HookType#AFTER}</span> * <dd> * <pre>{@code @@ -114,6 +115,13 @@ import java.lang.invoke.MethodType; * will be wrapped into their corresponding wrapper type (e.g. {@link Boolean}). * If the original method has return type {@code void}, this value will be * {@code null}. + * <p> + * Multiple {@link HookType#BEFORE} and {@link HookType#AFTER} hooks are + * allowed to reference the same target method. Exclusively one + * {@link HookType#REPLACE} hook may reference a target method, no other types + * allowed. Attention must be paid to not guide the Fuzzer in different + * directions via {@link Jazzer}'s {@code guideTowardsXY} methods in the + * different hooks. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -142,6 +150,11 @@ public @interface MethodHook { * The name of the class that contains the method that should be hooked, * as returned by {@link Class#getName()}. * <p> + * If an interface or abstract class is specified, also calls to all + * implementations and subclasses available on the classpath during startup + * are hooked, respectively. Interfaces and subclasses are not taken into + * account for concrete classes. + * <p> * Examples: * <p><ul> * <li>{@link String}: {@code "java.lang.String"} @@ -180,4 +193,15 @@ public @interface MethodHook { * @return the descriptor of the method to be hooked */ String targetMethodDescriptor() default ""; + + /** + * Array of additional classes to hook. + * <p> + * Hooks are applied on call sites. This means that classes calling the one + * defined in this annotation need to be instrumented to actually execute + * the hook. This property can be used to hook normally ignored classes. + * + * @return fully qualified class names to hook + */ + String[] additionalClassesToHook() default {}; } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java index 8c344621..3b0d046b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/FuzzTarget.java @@ -20,11 +20,18 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.utils.SimpleGlobMatcher; import com.code_intelligence.jazzer.utils.Utils; import java.io.Closeable; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -32,7 +39,12 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -public class FuzzTarget { +public final class FuzzTarget { + private static final String AUTOFUZZ_REPRODUCER_TEMPLATE = "public class Crash_%s {\n" + + " public static void main(String[] args) throws Throwable {\n" + + " %s;\n" + + " }\n" + + "}"; private static final long MAX_EXECUTIONS_WITHOUT_INVOCATION = 100; private static String methodReference; @@ -40,7 +52,6 @@ public class FuzzTarget { private static Map<Executable, Class<?>[]> throwsDeclarations; private static Set<SimpleGlobMatcher> ignoredExceptionMatchers; private static long executionsSinceLastInvocation = 0; - private static AutofuzzCodegenVisitor codegenVisitor; public static void fuzzerInitialize(String[] args) { if (args.length == 0 || !args[0].contains("::")) { @@ -73,19 +84,28 @@ public class FuzzTarget { descriptor = null; } - Class<?> targetClass; - try { - // Explicitly invoking static initializers to trigger some coverage in the code. - targetClass = Class.forName(className, true, ClassLoader.getSystemClassLoader()); - } catch (ClassNotFoundException e) { - System.err.printf( - "Failed to find class %s for autofuzz, please ensure it is contained in the classpath " - + "specified with --cp and specify the full package name%n", - className); - e.printStackTrace(); - System.exit(1); - return; - } + Class<?> targetClass = null; + String targetClassName = className; + do { + try { + // Explicitly invoking static initializers to trigger some coverage in the code. + targetClass = Class.forName(targetClassName, true, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + int classSeparatorIndex = targetClassName.lastIndexOf("."); + if (classSeparatorIndex == -1) { + System.err.printf( + "Failed to find class %s for autofuzz, please ensure it is contained in the classpath " + + "specified with --cp and specify the full package name%n", + className); + e.printStackTrace(); + System.exit(1); + return; + } + StringBuilder classNameBuilder = new StringBuilder(targetClassName); + classNameBuilder.setCharAt(classSeparatorIndex, '$'); + targetClassName = classNameBuilder.toString(); + } + } while (targetClass == null); boolean isConstructor = methodName.equals("new"); if (isConstructor) { @@ -96,8 +116,13 @@ public class FuzzTarget { || Utils.getReadableDescriptor(constructor).equals(descriptor)) .toArray(Executable[] ::new); } else { + // We use getDeclaredMethods and filter for the public access modifier instead of using + // getMethods as we want to exclude methods inherited from superclasses or interfaces, which + // can lead to unexpected results when autofuzzing. If desired, these can be autofuzzed + // explicitly instead. targetExecutables = - Arrays.stream(targetClass.getMethods()) + Arrays.stream(targetClass.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) .filter(method -> method.getName().equals(methodName) && (descriptor == null @@ -179,9 +204,36 @@ public class FuzzTarget { } public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Throwable { + AutofuzzCodegenVisitor codegenVisitor = null; if (Meta.isDebug()) { codegenVisitor = new AutofuzzCodegenVisitor(); } + fuzzerTestOneInput(data, codegenVisitor); + if (codegenVisitor != null) { + System.err.println(codegenVisitor.generate()); + } + } + + public static void dumpReproducer(FuzzedDataProvider data, String reproducerPath, String sha) { + AutofuzzCodegenVisitor codegenVisitor = new AutofuzzCodegenVisitor(); + try { + fuzzerTestOneInput(data, codegenVisitor); + } catch (Throwable ignored) { + } + String javaSource = String.format(AUTOFUZZ_REPRODUCER_TEMPLATE, sha, codegenVisitor.generate()); + Path javaPath = Paths.get(reproducerPath, String.format("Crash_%s.java", sha)); + try { + Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } catch (IOException e) { + System.err.printf("ERROR: Failed to write Java reproducer to %s%n", javaPath); + e.printStackTrace(); + } + System.out.printf( + "reproducer_path='%s'; Java reproducer written to %s%n", reproducerPath, javaPath); + } + + private static void fuzzerTestOneInput( + FuzzedDataProvider data, AutofuzzCodegenVisitor codegenVisitor) throws Throwable { Executable targetExecutable; if (FuzzTarget.targetExecutables.length == 1) { targetExecutable = FuzzTarget.targetExecutables[0]; @@ -196,9 +248,6 @@ public class FuzzTarget { returnValue = Meta.autofuzz(data, (Constructor<?>) targetExecutable, codegenVisitor); } executionsSinceLastInvocation = 0; - if (codegenVisitor != null) { - System.err.println(codegenVisitor.generate()); - } } catch (AutofuzzConstructionException e) { if (Meta.isDebug()) { e.printStackTrace(); @@ -245,8 +294,8 @@ public class FuzzTarget { } } - // Removes all stack trace elements that live in the Java standard library, internal JDK classes - // or the autofuzz package from the bottom of all stack frames. + // Removes all stack trace elements that live in the Java reflection packages or the autofuzz + // package from the bottom of all stack frames. private static void cleanStackTraces(Throwable t) { Throwable cause = t; while (cause != null) { @@ -255,8 +304,9 @@ public class FuzzTarget { for (firstInterestingPos = elements.length - 1; firstInterestingPos > 0; firstInterestingPos--) { String className = elements[firstInterestingPos].getClassName(); - if (!className.startsWith("com.code_intelligence.jazzer.autofuzz") - && !className.startsWith("java.") && !className.startsWith("jdk.")) { + if (!className.startsWith("com.code_intelligence.jazzer.autofuzz.") + && !className.startsWith("java.lang.reflect.") + && !className.startsWith("jdk.internal.reflect.")) { break; } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java index 96980530..3d48017f 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/autofuzz/Meta.java @@ -36,9 +36,14 @@ import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -46,11 +51,13 @@ import net.jodah.typetools.TypeResolver; import net.jodah.typetools.TypeResolver.Unknown; public class Meta { - static WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache = + static final WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache = new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache = + new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache = + new WeakHashMap<>(); + static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>(); - static WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>(); public static Object autofuzz(FuzzedDataProvider data, Method method) { return autofuzz(data, method, null); @@ -64,22 +71,29 @@ public class Meta { visitor.pushGroup( String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", ""); } - result = autofuzz(data, method, null, visitor); - if (visitor != null) { - visitor.popGroup(); + try { + result = autofuzz(data, method, null, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } } } else { if (visitor != null) { // This group will always have two elements: The thisObject and the method call. - visitor.pushGroup("", ".", ""); + // Since the this object can be a complex expression, wrap it in paranthesis. + visitor.pushGroup("(", ").", ""); } Object thisObject = consume(data, method.getDeclaringClass(), visitor); if (thisObject == null) { throw new AutofuzzConstructionException(); } - result = autofuzz(data, method, thisObject, visitor); - if (visitor != null) { - visitor.popGroup(); + try { + result = autofuzz(data, method, thisObject, visitor); + } finally { + if (visitor != null) { + visitor.popGroup(); + } } } return result; @@ -210,7 +224,13 @@ public class Meta { return consume(data, type, null); } - static Object consume(FuzzedDataProvider data, Class<?> type, AutofuzzCodegenVisitor visitor) { + // Invariant: The Java source code representation of the returned object visited by visitor must + // represent an object of the same type as genericType. For example, a null value returned for + // the genericType Class<java.lang.String> should lead to the generated code + // "(java.lang.String) null", not just "null". This makes it possible to safely use consume in + // recursive argument constructions. + static Object consume(FuzzedDataProvider data, Type genericType, AutofuzzCodegenVisitor visitor) { + Class<?> type = getRawType(genericType); if (type == byte.class || type == Byte.class) { byte result = data.consumeByte(); if (visitor != null) @@ -252,13 +272,18 @@ public class Meta { visitor.addCharLiteral(result); return result; } - // Return null for non-primitive and non-boxed types in ~5% of the cases. + // Sometimes, but rarely return null for non-primitive and non-boxed types. // TODO: We might want to return null for boxed types sometimes, but this is complicated by the // fact that TypeUtils can't distinguish between a primitive type and its wrapper and may // thus easily cause false-positive NullPointerExceptions. - if (!type.isPrimitive() && data.consumeByte((byte) 0, (byte) 19) == 0) { - if (visitor != null) - visitor.pushElement("null"); + if (!type.isPrimitive() && data.consumeByte() == 0) { + if (visitor != null) { + if (type == Object.class) { + visitor.pushElement("null"); + } else { + visitor.pushElement(String.format("(%s) null", type.getCanonicalName())); + } + } return null; } if (type == String.class || type == CharSequence.class) { @@ -344,6 +369,60 @@ public class Meta { ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})"))); } return new ByteArrayInputStream(array); + } else if (type == Map.class) { + ParameterizedType mapType = (ParameterizedType) genericType; + if (mapType.getActualTypeArguments().length != 2) { + throw new AutofuzzError( + "Expected Map generic type to have two type parameters: " + mapType); + } + Type keyType = mapType.getActualTypeArguments()[0]; + Type valueType = mapType.getActualTypeArguments()[1]; + if (visitor != null) { + // Do not use Collectors.toMap() since it cannot handle null values. + // Also annotate the type of the entry stream since it might be empty, in which case type + // inference on the accumulator could fail. + visitor.pushGroup( + String.format("java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<%s, %s>>of(", + keyType.getTypeName(), valueType.getTypeName()), + ", ", + ").collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)"); + } + int remainingBytesBeforeFirstEntryCreation = data.remainingBytes(); + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + Object firstKey = consume(data, keyType, visitor); + Object firstValue = consume(data, valueType, visitor); + if (visitor != null) { + visitor.popGroup(); + } + int remainingBytesAfterFirstEntryCreation = data.remainingBytes(); + int sizeOfElementEstimate = + remainingBytesBeforeFirstEntryCreation - remainingBytesAfterFirstEntryCreation; + int mapSize = consumeArrayLength(data, sizeOfElementEstimate); + Map<Object, Object> map = new HashMap<>(mapSize); + for (int i = 0; i < mapSize; i++) { + if (i == 0) { + map.put(firstKey, firstValue); + } else { + if (visitor != null) { + visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")"); + } + map.put(consume(data, keyType, visitor), consume(data, valueType, visitor)); + if (visitor != null) { + visitor.popGroup(); + } + } + } + if (visitor != null) { + if (mapSize == 0) { + // We implicitly pushed the first entry with the call to consume above, but it is not + // part of the array. + visitor.popElement(); + } + visitor.popGroup(); + } + return map; } else if (type.isEnum()) { Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants()); if (visitor != null) { @@ -578,7 +657,7 @@ public class Meta { FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) { Object[] result; try { - result = Arrays.stream(executable.getParameterTypes()) + result = Arrays.stream(executable.getGenericParameterTypes()) .map((type) -> consume(data, type, visitor)) .toArray(); return result; @@ -616,4 +695,22 @@ public class Meta { } return result; } + + private static Class<?> getRawType(Type genericType) { + if (genericType instanceof Class<?>) { + return (Class<?>) genericType; + } else if (genericType instanceof ParameterizedType) { + return getRawType(((ParameterizedType) genericType).getRawType()); + } else if (genericType instanceof WildcardType) { + // TODO: Improve this. + return Object.class; + } else if (genericType instanceof TypeVariable<?>) { + throw new AutofuzzError("Did not expect genericType to be a TypeVariable: " + genericType); + } else if (genericType instanceof GenericArrayType) { + // TODO: Improve this; + return Object[].class; + } else { + throw new AutofuzzError("Got unexpected class implementing Type: " + genericType); + } + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel deleted file mode 100644 index fceda64c..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/BUILD.bazel +++ /dev/null @@ -1,40 +0,0 @@ -java_binary( - name = "NoThrowDoclet", - srcs = ["NoThrowDoclet.java"], - create_executable = False, - tags = ["manual"], -) - -# To regenerate the list of methods, ensure that your local JDK is as recent as possible and contains `lib/src.zip`. -# This will be the case if you are using the release binaries of the OpenJDK or if the `openjdk-<version>-source` -# package is installed. -# Then, execute -# agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh -# from the Bazel root and copy the file into -# org.jacoco.core/src/org/jacoco/core/internal/flow/java_no_throw_methods_list.dat -# in the CodeIntelligenceTesting/jacoco repository. -genrule( - name = "java_no_throw_methods_list", - srcs = [ - "@local_jdk//:lib/src.zip", - ], - outs = [ - "java_no_throw_methods_list.dat.generated", - ], - cmd = """ - TMP=$$(mktemp -d) && \ - unzip $(execpath @local_jdk//:lib/src.zip) -d $$TMP && \ - $(execpath @local_jdk//:bin/javadoc) \ - -doclet com.code_intelligence.jazzer.generated.NoThrowDoclet \ - -docletpath $(execpath :NoThrowDoclet_deploy.jar) \ - --module java.base \ - --source-path $$TMP/java.base \ - --out $@ && \ - sort -o $@ $@ && \ - rm -rf $$TMP""", - tags = ["manual"], - tools = [ - ":NoThrowDoclet_deploy.jar", - "@local_jdk//:bin/javadoc", - ], -) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java b/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java deleted file mode 100644 index 1b52a228..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/generated/NoThrowDoclet.java +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.code_intelligence.jazzer.generated; - -import com.sun.source.doctree.DocCommentTree; -import com.sun.source.doctree.DocTree; -import com.sun.source.doctree.ThrowsTree; -import com.sun.source.util.DocTrees; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.ModuleElement; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; -import jdk.javadoc.doclet.Doclet; -import jdk.javadoc.doclet.DocletEnvironment; -import jdk.javadoc.doclet.Reporter; - -/** - * A Doclet that extracts a list of all method signatures in {@code java.*} that are declared not to - * throw any exceptions, including {@link RuntimeException} but excluding {@link - * VirtualMachineError}. - * - * Crucially, whereas the throws declaration of a method does not contain subclasses of {@link - * RuntimeException}, the {@code @throws} Javadoc tag does. - */ -public class NoThrowDoclet implements Doclet { - private BufferedWriter out; - - @Override - public void init(Locale locale, Reporter reporter) {} - - @Override - public String getName() { - return getClass().getSimpleName(); - } - - @Override - public Set<? extends Option> getSupportedOptions() { - return Set.of(new Option() { - @Override - public int getArgumentCount() { - return 1; - } - - @Override - public String getDescription() { - return "Output file (.kt)"; - } - - @Override - public Kind getKind() { - return Kind.STANDARD; - } - - @Override - public List<String> getNames() { - return List.of("--out"); - } - - @Override - public String getParameters() { - return "<output file (.kt)>"; - } - - @Override - public boolean process(String option, List<String> args) { - try { - out = new BufferedWriter(new FileWriter(args.get(0))); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } - }); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return null; - } - - private String toDescriptor(TypeMirror type) { - switch (type.getKind()) { - case BOOLEAN: - return "Z"; - case BYTE: - return "B"; - case CHAR: - return "C"; - case DOUBLE: - return "D"; - case FLOAT: - return "F"; - case INT: - return "I"; - case LONG: - return "J"; - case SHORT: - return "S"; - case VOID: - return "V"; - case ARRAY: - return "[" + toDescriptor(((ArrayType) type).getComponentType()); - case DECLARED: - return "L" + getFullyQualifiedName((DeclaredType) type) + ";"; - case TYPEVAR: - return "Ljava/lang/Object;"; - } - throw new IllegalArgumentException( - "Unexpected kind " + type.getKind() + ": " + type.toString()); - } - - private String getFullyQualifiedName(DeclaredType declaredType) { - TypeElement element = (TypeElement) declaredType.asElement(); - return element.getQualifiedName().toString().replace('.', '/'); - } - - private void handleExecutableElement(DocTrees trees, ExecutableElement executable) - throws IOException { - if (!executable.getModifiers().contains(Modifier.PUBLIC)) - return; - - DocCommentTree tree = trees.getDocCommentTree(executable); - if (tree != null) { - for (DocTree tag : tree.getBlockTags()) { - if (tag instanceof ThrowsTree) { - return; - } - } - } - - String methodName = executable.getSimpleName().toString(); - String className = - ((TypeElement) executable.getEnclosingElement()).getQualifiedName().toString(); - String internalClassName = className.replace('.', '/'); - - String paramTypeDescriptors = executable.getParameters() - .stream() - .map(VariableElement::asType) - .map(this::toDescriptor) - .collect(Collectors.joining("")); - String returnTypeDescriptor = toDescriptor(executable.getReturnType()); - String methodDescriptor = String.format("(%s)%s", paramTypeDescriptors, returnTypeDescriptor); - - out.write(String.format("%s#%s#%s%n", internalClassName, methodName, methodDescriptor)); - } - - public void handleTypeElement(DocTrees trees, TypeElement typeElement) throws IOException { - List<ExecutableElement> executables = - ElementFilter.constructorsIn(typeElement.getEnclosedElements()); - executables.addAll(ElementFilter.methodsIn(typeElement.getEnclosedElements())); - for (ExecutableElement executableElement : executables) { - handleExecutableElement(trees, executableElement); - } - } - - @Override - public boolean run(DocletEnvironment docletEnvironment) { - try { - DocTrees trees = docletEnvironment.getDocTrees(); - for (ModuleElement moduleElement : - ElementFilter.modulesIn(docletEnvironment.getSpecifiedElements())) { - for (PackageElement packageElement : - ElementFilter.packagesIn(moduleElement.getEnclosedElements())) { - if (packageElement.getQualifiedName().toString().startsWith("java.")) { - for (TypeElement typeElement : - ElementFilter.typesIn(packageElement.getEnclosedElements())) { - ElementKind kind = typeElement.getKind(); - if (kind == ElementKind.CLASS || kind == ElementKind.ENUM - || kind == ElementKind.INTERFACE) { - handleTypeElement(trees, typeElement); - } - } - } - } - } - } catch (IOException e) { - e.printStackTrace(); - return false; - } - try { - out.close(); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } -}
\ No newline at end of file diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 50d10705..db93dcae 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -12,34 +12,24 @@ kt_jvm_library( "Hook.kt", "HookInstrumentor.kt", "HookMethodVisitor.kt", + "Hooks.kt", "Instrumentor.kt", + "StaticMethodStrategy.java", "TraceDataFlowInstrumentor.kt", ], visibility = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", "//agent/src/main/java/com/code_intelligence/jazzer/agent:__pkg__", "//agent/src/test/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", ], deps = [ - ":shaded_deps", "//agent/src/main/java/com/code_intelligence/jazzer/runtime", "//agent/src/main/java/com/code_intelligence/jazzer/utils", "@com_github_classgraph_classgraph//:classgraph", "@com_github_jetbrains_kotlin//:kotlin-reflect", - ], -) - -jar_jar( - name = "shaded_deps", - input_jar = "unshaded_deps_deploy.jar", - rules = "shade_rules", -) - -java_binary( - name = "unshaded_deps", - create_executable = False, - runtime_deps = [ "@jazzer_jacoco//:jacoco_internal", - "@jazzer_ow2_asm//:asm", - "@jazzer_ow2_asm//:asm_commons", + "@org_ow2_asm_asm//jar", + "@org_ow2_asm_asm_commons//jar", ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt index f6728a1a..4c3eabcb 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt @@ -14,6 +14,8 @@ 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) } @@ -24,7 +26,11 @@ class ClassInstrumentor constructor(bytecode: ByteArray) { private set fun coverage(initialEdgeId: Int): Int { - val edgeCoverageInstrumentor = EdgeCoverageInstrumentor(initialEdgeId) + val edgeCoverageInstrumentor = EdgeCoverageInstrumentor( + defaultEdgeCoverageStrategy, + defaultCoverageMap, + initialEdgeId, + ) instrumentedBytecode = edgeCoverageInstrumentor.instrument(instrumentedBytecode) return edgeCoverageInstrumentor.numEdges } @@ -41,13 +47,7 @@ class ClassInstrumentor constructor(bytecode: ByteArray) { } companion object { - init { - try { - // Calls JNI_OnLoad_jazzer_initialize in the driver, which registers the native methods. - System.loadLibrary("jazzer_initialize") - } catch (_: UnsatisfiedLinkError) { - // Make it possible to use (parts of) the agent without the driver. - } - } + val defaultEdgeCoverageStrategy = StaticMethodStrategy() + val defaultCoverageMap = CoverageMap::class.java } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt index 65956189..098cf389 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/CoverageRecorder.kt @@ -15,18 +15,17 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.runtime.CoverageMap -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.CoverageBuilder -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionData -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataReader -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataWriter -import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfo -import com.code_intelligence.jazzer.third_party.jacoco.core.data.SessionInfoStore -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.data.CRC64 +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.CoverageBuilder +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataWriter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfo +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.data.CRC64 import com.code_intelligence.jazzer.utils.ClassNameGlobber import io.github.classgraph.ClassGraph -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream import java.time.Instant import java.util.UUID @@ -52,26 +51,26 @@ object CoverageRecorder { } /** - * Manually records coverage IDs based on the current state of [CoverageMap.mem]. + * Manually records coverage IDs based on the current state of [CoverageMap]. * Should be called after static initializers have run. */ @JvmStatic fun updateCoveredIdsWithCoverageMap() { - val mem = CoverageMap.mem - val size = mem.capacity() - additionalCoverage.addAll((0 until size).filter { mem[it] > 0 }) + additionalCoverage.addAll(CoverageMap.getCoveredIds()) } + /** + * [dumpCoverageReport] dumps a human-readable coverage report of files using any [coveredIds] to [dumpFileName]. + */ @JvmStatic - fun replayCoveredIds() { - val mem = CoverageMap.mem - for (coverageId in additionalCoverage) { - mem.put(coverageId, 1) + fun dumpCoverageReport(coveredIds: IntArray, dumpFileName: String) { + File(dumpFileName).bufferedWriter().use { writer -> + writer.write(computeFileCoverage(coveredIds)) } } - @JvmStatic - fun computeFileCoverage(coveredIds: IntArray): String { + private fun computeFileCoverage(coveredIds: IntArray): String { + fun Double.format(digits: Int) = "%.${digits}f".format(this) val coverage = analyzeCoverage(coveredIds.toSet()) ?: return "No classes were instrumented" return coverage.sourceFiles.joinToString( "\n", @@ -109,21 +108,42 @@ object CoverageRecorder { } } - private fun Double.format(digits: Int) = "%.${digits}f".format(this) + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [dumpFileName]. + * JaCoCo only exports coverage for files containing at least one coverage data point. The dump + * can be used by the JaCoCo report command to create reports also including not covered files. + */ + @JvmStatic + fun dumpJacocoCoverage(coveredIds: IntArray, dumpFileName: String) { + FileOutputStream(dumpFileName).use { outStream -> + dumpJacocoCoverage(coveredIds, outStream) + } + } + + /** + * [dumpJacocoCoverage] dumps the JaCoCo coverage of files using any [coveredIds] to [outStream]. + */ + @JvmStatic + fun dumpJacocoCoverage(coveredIds: IntArray, outStream: OutputStream) { + // Return if no class has been instrumented. + val startTimestamp = startTimestamp ?: return - fun dumpJacocoCoverage(coveredIds: Set<Int>): ByteArray? { // Update the list of covered IDs with the coverage information for the current run. updateCoveredIdsWithCoverageMap() val dumpTimestamp = Instant.now() - val outStream = ByteArrayOutputStream() val outWriter = ExecutionDataWriter(outStream) - // Return null if no class has been instrumented. - val startTimestamp = startTimestamp ?: return null outWriter.visitSessionInfo( SessionInfo(UUID.randomUUID().toString(), startTimestamp.epochSecond, dumpTimestamp.epochSecond) ) + analyzeJacocoCoverage(coveredIds.toSet()).accept(outWriter) + } + /** + * Build up a JaCoCo [ExecutionDataStore] based on [coveredIds] containing the internally gathered coverage information. + */ + private fun analyzeJacocoCoverage(coveredIds: Set<Int>): ExecutionDataStore { + val executionDataStore = ExecutionDataStore() val sortedCoveredIds = (additionalCoverage + coveredIds).sorted().toIntArray() for ((internalClassName, info) in instrumentedClassInfo) { // Determine the subarray of coverage IDs in sortedCoveredIds that contains the IDs generated while @@ -153,32 +173,27 @@ object CoverageRecorder { .forEach { classLocalEdgeId -> probes[classLocalEdgeId] = true } - outWriter.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) + executionDataStore.visitClassExecution(ExecutionData(info.classId, internalClassName, probes)) } - return outStream.toByteArray() + return executionDataStore } + /** + * Create a [CoverageBuilder] containing all classes matching the include/exclude pattern and their coverage statistics. + */ fun analyzeCoverage(coveredIds: Set<Int>): CoverageBuilder? { return try { val coverage = CoverageBuilder() analyzeAllUncoveredClasses(coverage) - val rawExecutionData = dumpJacocoCoverage(coveredIds) ?: return null - val executionDataStore = ExecutionDataStore() - val sessionInfoStore = SessionInfoStore() - ByteArrayInputStream(rawExecutionData).use { stream -> - ExecutionDataReader(stream).run { - setExecutionDataVisitor(executionDataStore) - setSessionInfoVisitor(sessionInfoStore) - read() - } - } + val executionDataStore = analyzeJacocoCoverage(coveredIds) for ((internalClassName, info) in instrumentedClassInfo) { - EdgeCoverageInstrumentor(0).analyze( - executionDataStore, - coverage, - info.bytecode, - internalClassName - ) + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0) + .analyze( + executionDataStore, + coverage, + info.bytecode, + internalClassName + ) } coverage } catch (e: Exception) { @@ -198,7 +213,6 @@ object CoverageRecorder { .asSequence() .map { it.replace('/', '.') } .toSet() - val emptyExecutionDataStore = ExecutionDataStore() ClassGraph() .enableClassInfo() .ignoreClassVisibility() @@ -209,13 +223,16 @@ object CoverageRecorder { "jaz", ) .scan().use { result -> + // ExecutionDataStore is used to look up existing coverage during analysis of the class files, + // no entries are added during that. Passing in an empty store is fine for uncovered files. + val emptyExecutionDataStore = ExecutionDataStore() result.allClasses .asSequence() .filter { classInfo -> classNameGlobber.includes(classInfo.name) } .filterNot { classInfo -> classInfo.name in coveredClassNames } .forEach { classInfo -> classInfo.resource.use { resource -> - EdgeCoverageInstrumentor(0).analyze( + EdgeCoverageInstrumentor(ClassInstrumentor.defaultEdgeCoverageStrategy, ClassInstrumentor.defaultCoverageMap, 0).analyze( emptyExecutionDataStore, coverage, resource.load(), diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt index ba5b7ee9..8fb3dc2b 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt @@ -14,37 +14,92 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.runtime.CoverageMap -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.Analyzer -import com.code_intelligence.jazzer.third_party.jacoco.core.analysis.ICoverageVisitor -import com.code_intelligence.jazzer.third_party.jacoco.core.data.ExecutionDataStore -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesAdapter -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.ClassProbesVisitor -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.IClassProbesAdapterFactory -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.flow.JavaNoThrowMethods -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ClassInstrumenter -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeArrayStrategy -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.IProbeInserterFactory -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.InstrSupport -import com.code_intelligence.jazzer.third_party.jacoco.core.internal.instr.ProbeInserter -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.Analyzer +import com.code_intelligence.jazzer.third_party.org.jacoco.core.analysis.ICoverageVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesAdapter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.ClassProbesVisitor +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.flow.IClassProbesAdapterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ClassInstrumenter +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeArrayStrategy +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.IProbeInserterFactory +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.ProbeInserter +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles.publicLookup +import java.lang.invoke.MethodType.methodType import kotlin.math.max +/** + * A particular way to instrument bytecode for edge coverage using a coverage map class available to + * hold the collected coverage data at runtime. + */ +interface EdgeCoverageStrategy { + + /** + * Inject bytecode instrumentation on a control flow edge with ID [edgeId], with access to the + * local variable [variable] that is populated at the beginning of each method by the + * instrumentation injected in [loadLocalVariable]. + */ + fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) + + /** + * The maximal number of stack elements used by [instrumentControlFlowEdge]. + */ + val instrumentControlFlowEdgeStackSize: Int + + /** + * The type of the local variable used by the instrumentation in the format used by + * [MethodVisitor.visitFrame]'s `local` parameter, or `null` if the instrumentation does not use + * one. + * @see https://asm.ow2.io/javadoc/org/objectweb/asm/MethodVisitor.html#visitFrame(int,int,java.lang.Object%5B%5D,int,java.lang.Object%5B%5D) + */ + val localVariableType: Any? + + /** + * Inject bytecode that loads the coverage counters of the coverage map class described by + * [coverageMapInternalClassName] into the local variable [variable]. + */ + fun loadLocalVariable(mv: MethodVisitor, variable: Int, coverageMapInternalClassName: String) + + /** + * The maximal number of stack elements used by [loadLocalVariable]. + */ + val loadLocalVariableStackSize: Int +} + +// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it +// internally tracks the edge IDs, which have to be globally unique. class EdgeCoverageInstrumentor( + private val strategy: EdgeCoverageStrategy, + /** + * The class must have the following public static member + * - method enlargeIfNeeded(int nextEdgeId): Called before a new edge ID is emitted. + */ + coverageMapClass: Class<*>, private val initialEdgeId: Int, - private val coverageMapClass: Class<*> = CoverageMap::class.java ) : Instrumentor { private var nextEdgeId = initialEdgeId + private val coverageMapInternalClassName = coverageMapClass.name.replace('.', '/') - init { - if (isTesting) { - JavaNoThrowMethods.isTesting = true - } - } + private val enlargeIfNeeded: MethodHandle = + publicLookup().findStatic( + coverageMapClass, + "enlargeIfNeeded", + methodType( + Void::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + ) override fun instrument(bytecode: ByteArray): ByteArray { val reader = InstrSupport.classReaderFor(bytecode) @@ -67,93 +122,14 @@ class EdgeCoverageInstrumentor( val numEdges get() = nextEdgeId - initialEdgeId - private val isTesting - get() = coverageMapClass != CoverageMap::class.java - private fun nextEdgeId(): Int { - if (nextEdgeId >= CoverageMap.mem.capacity()) { - if (!isTesting) { - CoverageMap.enlargeCoverageMap() - } - } + enlargeIfNeeded.invokeExact(nextEdgeId) return nextEdgeId++ } /** - * The maximal number of stack elements used by [loadCoverageMap]. - */ - private val loadCoverageMapStackSize = 1 - - /** - * Inject bytecode that loads the coverage map into local variable [variable]. - */ - private fun loadCoverageMap(mv: MethodVisitor, variable: Int) { - mv.apply { - visitFieldInsn( - Opcodes.GETSTATIC, - coverageMapInternalClassName, - "mem", - "Ljava/nio/ByteBuffer;" - ) - // Stack: mem (maxStack: 1) - visitVarInsn(Opcodes.ASTORE, variable) - } - } - - /** - * The maximal number of stack elements used by [instrumentControlFlowEdge]. - */ - private val instrumentControlFlowEdgeStackSize = 5 - - /** - * Inject bytecode instrumentation on a control flow edge with ID [edgeId]. The coverage map can be loaded from - * local variable [variable]. - */ - private fun instrumentControlFlowEdge(mv: MethodVisitor, edgeId: Int, variable: Int) { - mv.apply { - visitVarInsn(Opcodes.ALOAD, variable) - // Stack: mem - push(edgeId) - // Stack: mem | edgeId - visitInsn(Opcodes.DUP2) - // Stack: mem | edgeId | mem | edgeId - visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "get", "(I)B", false) - // Increment the counter, but ensure that it never stays at 0 after an overflow by incrementing it again in - // that case. - // This approach performs better than saturating the counter at 255 (see Section 3.3 of - // https://www.usenix.org/system/files/woot20-paper-fioraldi.pdf) - // Stack: mem | edgeId | counter (sign-extended to int) - push(0xff) - // Stack: mem | edgeId | counter (sign-extended to int) | 0x000000ff - visitInsn(Opcodes.IAND) - // Stack: mem | edgeId | counter (zero-extended to int) - push(1) - // Stack: mem | edgeId | counter | 1 - visitInsn(Opcodes.IADD) - // Stack: mem | edgeId | counter + 1 - visitInsn(Opcodes.DUP) - // Stack: mem | edgeId | counter + 1 | counter + 1 - push(8) - // Stack: mem | edgeId | counter + 1 | counter + 1 | 8 (maxStack: +5) - visitInsn(Opcodes.ISHR) - // Stack: mem | edgeId | counter + 1 | 1 if the increment overflowed to 0, 0 otherwise - visitInsn(Opcodes.IADD) - // Stack: mem | edgeId | counter + 2 if the increment overflowed, counter + 1 otherwise - visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/nio/ByteBuffer", "put", "(IB)Ljava/nio/ByteBuffer;", false) - // Stack: mem - visitInsn(Opcodes.POP) - if (isTesting) { - visitMethodInsn(Opcodes.INVOKESTATIC, coverageMapInternalClassName, "updated", "()V", false) - } - } - } - -// The remainder of this file interfaces with classes in org.jacoco.core.internal. Changes to this part should not be -// necessary unless JaCoCo is updated or the way we instrument for coverage changes fundamentally. - - /** - * A [ProbeInserter] that injects the bytecode instrumentation returned by [instrumentControlFlowEdge] and modifies - * the stack size and number of local variables accordingly. + * A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and + * modifies the stack size and number of local variables accordingly. */ private inner class EdgeCoverageProbeInserter( access: Int, @@ -163,13 +139,16 @@ class EdgeCoverageInstrumentor( arrayStrategy: IProbeArrayStrategy, ) : ProbeInserter(access, name, desc, mv, arrayStrategy) { override fun insertProbe(id: Int) { - instrumentControlFlowEdge(mv, id, variable) + strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName) } override fun visitMaxs(maxStack: Int, maxLocals: Int) { - val newMaxStack = max(maxStack + instrumentControlFlowEdgeStackSize, loadCoverageMapStackSize) - mv.visitMaxs(newMaxStack, maxLocals + 1) + val newMaxStack = max(maxStack + strategy.instrumentControlFlowEdgeStackSize, strategy.loadLocalVariableStackSize) + val newMaxLocals = maxLocals + if (strategy.localVariableType != null) 1 else 0 + mv.visitMaxs(newMaxStack, newMaxLocals) } + + override fun getLocalVariableType() = strategy.localVariableType } private val edgeCoverageProbeInserterFactory = @@ -177,9 +156,16 @@ class EdgeCoverageInstrumentor( EdgeCoverageProbeInserter(access, name, desc, mv, arrayStrategy) } - private inner class EdgeCoverageClassProbesAdapter(cv: ClassProbesVisitor, trackFrames: Boolean) : - ClassProbesAdapter(cv, trackFrames) { + private inner class EdgeCoverageClassProbesAdapter(private val cpv: ClassProbesVisitor, trackFrames: Boolean) : + ClassProbesAdapter(cpv, trackFrames) { override fun nextId(): Int = nextEdgeId() + + override fun visitEnd() { + cpv.visitTotalProbeCount(numEdges) + // Avoid calling super.visitEnd() as that invokes cpv.visitTotalProbeCount with an + // incorrect value of `count`. + cv.visitEnd() + } } private val edgeCoverageClassProbesAdapterFactory = IClassProbesAdapterFactory { probesVisitor, trackFrames -> @@ -188,14 +174,14 @@ class EdgeCoverageInstrumentor( private val edgeCoverageProbeArrayStrategy = object : IProbeArrayStrategy { override fun storeInstance(mv: MethodVisitor, clinit: Boolean, variable: Int): Int { - loadCoverageMap(mv, variable) - return loadCoverageMapStackSize + strategy.loadLocalVariable(mv, variable, coverageMapInternalClassName) + return strategy.loadLocalVariableStackSize } override fun addMembers(cv: ClassVisitor, probeCount: Int) {} } +} - private fun MethodVisitor.push(value: Int) { - InstrSupport.push(this, value) - } +fun MethodVisitor.push(value: Int) { + InstrSupport.push(this, value) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt index 92106e14..ff68ad94 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hook.kt @@ -18,46 +18,65 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.api.HookType import com.code_intelligence.jazzer.api.MethodHook -import com.code_intelligence.jazzer.api.MethodHooks import com.code_intelligence.jazzer.utils.descriptor import java.lang.invoke.MethodHandle import java.lang.reflect.Method import java.lang.reflect.Modifier -class Hook private constructor(hookMethod: Method, annotation: MethodHook) { - // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround - // for mangled hooks due to shading applied to hooks. - private val targetClassName = annotation.targetClassName.trim() - val targetMethodName = annotation.targetMethod - val targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotEmpty() } - val hookType = annotation.type - - val targetInternalClassName = targetClassName.replace('.', '/') - private val targetReturnTypeDescriptor = targetMethodDescriptor?.let { extractReturnTypeDescriptor(it) } - private val targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) } - - private val hookClassName: String = hookMethod.declaringClass.name - val hookInternalClassName = hookClassName.replace('.', '/') - val hookMethodName: String = hookMethod.name - val hookMethodDescriptor = hookMethod.descriptor +class Hook private constructor( + private val targetClassName: String, + val hookType: HookType, + val targetMethodName: String, + val targetMethodDescriptor: String?, + val additionalClassesToHook: List<String>, + val targetInternalClassName: String, + private val targetReturnTypeDescriptor: String?, + private val targetWrappedReturnTypeDescriptor: String?, + private val hookClassName: String, + val hookInternalClassName: String, + val hookMethodName: String, + val hookMethodDescriptor: String +) { override fun toString(): String { - return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName" + return "$hookType $targetClassName.$targetMethodName: $hookClassName.$hookMethodName $additionalClassesToHook" } companion object { + fun createAndVerifyHook(hookMethod: Method, hookData: MethodHook, className: String): Hook { + return createHook(hookMethod, hookData, className).also { + verify(hookMethod, it) + } + } - fun verifyAndGetHook(hookMethod: Method, hookData: MethodHook): Hook { - // Verify the annotation type and extract information for debug statements. - val potentialHook = Hook(hookMethod, hookData) + private fun createHook(hookMethod: Method, annotation: MethodHook, targetClassName: String): Hook { + val targetReturnTypeDescriptor = annotation.targetMethodDescriptor + .takeIf { it.isNotBlank() }?.let { extractReturnTypeDescriptor(it) } + val hookClassName: String = hookMethod.declaringClass.name + return Hook( + targetClassName = targetClassName, + hookType = annotation.type, + targetMethodName = annotation.targetMethod, + targetMethodDescriptor = annotation.targetMethodDescriptor.takeIf { it.isNotBlank() }, + additionalClassesToHook = annotation.additionalClassesToHook.asList(), + targetInternalClassName = targetClassName.replace('.', '/'), + targetReturnTypeDescriptor = targetReturnTypeDescriptor, + targetWrappedReturnTypeDescriptor = targetReturnTypeDescriptor?.let { getWrapperTypeDescriptor(it) }, + hookClassName = hookClassName, + hookInternalClassName = hookClassName.replace('.', '/'), + hookMethodName = hookMethod.name, + hookMethodDescriptor = hookMethod.descriptor + ) + } + private fun verify(hookMethod: Method, potentialHook: Hook) { // Verify the hook method's modifiers (public static). require(Modifier.isPublic(hookMethod.modifiers)) { "$potentialHook: hook method must be public" } require(Modifier.isStatic(hookMethod.modifiers)) { "$potentialHook: hook method must be static" } // Verify the hook method's parameter count. val numParameters = hookMethod.parameters.size - when (hookData.type) { + when (potentialHook.hookType) { HookType.BEFORE, HookType.REPLACE -> require(numParameters == 4) { "$potentialHook: incorrect number of parameters (expected 4)" } HookType.AFTER -> require(numParameters == 5) { "$potentialHook: incorrect number of parameters (expected 5)" } } @@ -70,17 +89,18 @@ class Hook private constructor(hookMethod: Method, annotation: MethodHook) { require(parameterTypes[3] == Int::class.javaPrimitiveType) { "$potentialHook: fourth parameter must have type int" } // Verify the hook method's return type if possible. - when (hookData.type) { + when (potentialHook.hookType) { HookType.BEFORE, HookType.AFTER -> require(hookMethod.returnType == Void.TYPE) { "$potentialHook: return type must be void" } HookType.REPLACE -> if (potentialHook.targetReturnTypeDescriptor != null) { - val returnTypeDescriptor = hookMethod.returnType.descriptor - if (potentialHook.targetReturnTypeDescriptor == "V") { - require(returnTypeDescriptor == "V") { "$potentialHook: return type must be void to match targetMethodDescriptor" } + if (potentialHook.targetMethodName == "<init>") { + require(hookMethod.returnType.name == potentialHook.targetClassName) { "$potentialHook: return type must be ${potentialHook.targetClassName} to match target constructor" } + } else if (potentialHook.targetReturnTypeDescriptor == "V") { + require(hookMethod.returnType.descriptor == "V") { "$potentialHook: return type must be void" } } else { require( - returnTypeDescriptor in listOf( + hookMethod.returnType.descriptor in listOf( java.lang.Object::class.java.descriptor, potentialHook.targetReturnTypeDescriptor, potentialHook.targetWrappedReturnTypeDescriptor @@ -92,28 +112,22 @@ class Hook private constructor(hookMethod: Method, annotation: MethodHook) { } } - // AfterMethodHook only: Verify the type of the last parameter if known. - if (hookData.type == HookType.AFTER && potentialHook.targetReturnTypeDescriptor != null) { - require( - parameterTypes[4] == java.lang.Object::class.java || - parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor - ) { - "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}" + // AfterMethodHook only: Verify the type of the last parameter if known. Even if not + // known, it must not be a primitive value. + if (potentialHook.hookType == HookType.AFTER) { + if (potentialHook.targetReturnTypeDescriptor != null) { + require( + parameterTypes[4] == java.lang.Object::class.java || + parameterTypes[4].descriptor == potentialHook.targetWrappedReturnTypeDescriptor + ) { + "$potentialHook: fifth parameter must have type Object or match the descriptor ${potentialHook.targetWrappedReturnTypeDescriptor}" + } + } else { + require(!parameterTypes[4].isPrimitive) { + "$potentialHook: fifth parameter must not be a primitive type, use a boxed type instead" + } } } - - return potentialHook - } - } -} - -fun loadHooks(hookClass: Class<*>): List<Hook> { - val hooks = mutableListOf<Hook>() - for (method in hookClass.methods) { - method.getAnnotation(MethodHook::class.java)?.let { hooks.add(Hook.verifyAndGetHook(method, it)) } - method.getAnnotation(MethodHooks::class.java)?.let { - it.value.forEach { hookAnnotation -> hooks.add(Hook.verifyAndGetHook(method, hookAnnotation)) } } } - return hooks } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt index ac5f1780..6db76605 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookInstrumentor.kt @@ -14,10 +14,10 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor internal class HookInstrumentor(private val hooks: Iterable<Hook>, private val java6Mode: Boolean) : Instrumentor { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt index 7c23c703..1694be58 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/HookMethodVisitor.kt @@ -15,11 +15,12 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.api.HookType -import com.code_intelligence.jazzer.third_party.objectweb.asm.Handle -import com.code_intelligence.jazzer.third_party.objectweb.asm.MethodVisitor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.Type -import com.code_intelligence.jazzer.third_party.objectweb.asm.commons.LocalVariablesSorter +import org.objectweb.asm.Handle +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.LocalVariablesSorter +import java.util.concurrent.atomic.AtomicBoolean internal fun makeHookMethodVisitor( access: Int, @@ -41,6 +42,10 @@ private class HookMethodVisitor( private val random: DeterministicRandom, ) : MethodVisitor(Instrumentor.ASM_API_VERSION, methodVisitor) { + companion object { + private val showUnsupportedHookWarning = AtomicBoolean(true) + } + val lvs = object : LocalVariablesSorter(Instrumentor.ASM_API_VERSION, access, descriptor, this) { override fun updateNewLocals(newLocals: Array<Any>) { // The local variables involved in calling hooks do not need to outlive the current @@ -51,7 +56,7 @@ private class HookMethodVisitor( } } - private val hooks = hooks.associateBy { hook -> + private val hooks = hooks.groupBy { hook -> var hookKey = "${hook.hookType}#${hook.targetInternalClassName}#${hook.targetMethodName}" if (hook.targetMethodDescriptor != null) hookKey += "#${hook.targetMethodDescriptor}" @@ -69,63 +74,23 @@ private class HookMethodVisitor( mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) return } - handleMethodInsn(HookType.BEFORE, opcode, owner, methodName, methodDescriptor, isInterface) - } - - /** - * Emits the bytecode for a method call instruction for the next applicable hook type in order (BEFORE, REPLACE, - * AFTER). Since the instrumented code is indistinguishable from an uninstrumented call instruction, it can be - * safely nested. Combining REPLACE hooks with other hooks is however not supported as these hooks already subsume - * the functionality of BEFORE and AFTER hooks. - */ - private fun visitNextHookTypeOrCall( - hookType: HookType, - appliedHook: Boolean, - opcode: Int, - owner: String, - methodName: String, - methodDescriptor: String, - isInterface: Boolean, - ) = when (hookType) { - HookType.BEFORE -> { - val nextHookType = if (appliedHook) { - // After a BEFORE hook has been applied, we can safely apply an AFTER hook by replacing the actual - // call instruction with the full bytecode injected for the AFTER hook. - HookType.AFTER - } else { - // If no BEFORE hook is registered, look for a REPLACE hook next. - HookType.REPLACE - } - handleMethodInsn(nextHookType, opcode, owner, methodName, methodDescriptor, isInterface) - } - HookType.REPLACE -> { - // REPLACE hooks can't (and don't need to) be mixed with other hooks. We only cycle through them if we - // couldn't find a matching REPLACE hook, in which case we try an AFTER hook next. - require(!appliedHook) - handleMethodInsn(HookType.AFTER, opcode, owner, methodName, methodDescriptor, isInterface) - } - // An AFTER hook is always the last in the chain. Whether a hook has been applied or not, always emit the - // actual call instruction. - HookType.AFTER -> mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + handleMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) } fun handleMethodInsn( - hookType: HookType, opcode: Int, owner: String, methodName: String, methodDescriptor: String, isInterface: Boolean, ) { - val hook = findMatchingHook(hookType, owner, methodName, methodDescriptor) - if (hook == null) { - visitNextHookTypeOrCall(hookType, false, opcode, owner, methodName, methodDescriptor, isInterface) + val matchingHooks = findMatchingHooks(owner, methodName, methodDescriptor) + + if (matchingHooks.isEmpty()) { + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) return } - // The hookId is used to identify a call site. - val hookId = random.nextInt() - val paramDescriptors = extractParameterTypeDescriptors(methodDescriptor) val localObjArr = storeMethodArguments(paramDescriptors) // If the method we're hooking is not static there is now a reference to @@ -142,138 +107,158 @@ private class HookMethodVisitor( // We now removed all values for the original method call from the operand stack // and saved them to local variables. - // Start to build the arguments for the hook method. - if (methodName == "<init>") { - // Special case for constructors: - // We cannot create a MethodHandle for a constructor, so we push null instead. - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref - // Only pass the this object if it has been initialized by the time the hook is invoked. - if (hook.hookType == HookType.AFTER) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) - } else { - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref - } - } else { - // Push a MethodHandle representing the hooked method. - val handleOpcode = when (opcode) { - Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL - Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE - Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC - Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL - else -> -1 - } - if (java6Mode) { - // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50). + val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) + // Create a local variable to store the return value + val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor))) + + matchingHooks.forEachIndexed { index, hook -> + // The hookId is used to identify a call site. + val hookId = random.nextInt() + + // Start to build the arguments for the hook method. + if (methodName == "<init>") { + // Constructor is invoked on an uninitialized object, and that's still on the stack. + // In case of REPLACE pop it from the stack and replace it afterwards with the returned + // one from the hook. + if (hook.hookType == HookType.REPLACE) { + mv.visitInsn(Opcodes.POP) + } + // Special case for constructors: + // We cannot create a MethodHandle for a constructor, so we push null instead. mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + // Only pass the this object if it has been initialized by the time the hook is invoked. + if (hook.hookType == HookType.AFTER) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) + } else { + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } } else { - mv.visitLdcInsn( - Handle( - handleOpcode, - owner, - methodName, - methodDescriptor, - isInterface - ) - ) // push MethodHandle - } - // Stack layout: ... | MethodHandle (objectref) - // Push the owner object again - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) - } - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) - // Push a reference to our object array with the saved arguments - mv.visitVarInsn(Opcodes.ALOAD, localObjArr) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) - // Push the hook id - mv.visitLdcInsn(hookId) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // How we proceed depends on the type of hook we want to implement - when (hook.hookType) { - HookType.BEFORE -> { - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... - // Push the values for the original method call onto the stack again - if (opcode != Opcodes.INVOKESTATIC) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + // 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 } - loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments - // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... - // Call the original method or the next hook in order. - visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface) + if (java6Mode) { + // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50). + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } else { + mv.visitLdcInsn( + Handle( + handleOpcode, + owner, + methodName, + methodDescriptor, + isInterface + ) + ) // push MethodHandle + } + // Stack layout: ... | MethodHandle (objectref) + // Push the owner object again + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) } - HookType.REPLACE -> { - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... | [return value (primitive/objectref)] - // Check if we need to process the return value - val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) - if (returnTypeDescriptor != "V") { - val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor) - // if the hook method's return type is primitive we don't need to unwrap or cast it - if (!isPrimitiveType(hookMethodReturnType)) { - // Check if the returned object type is different than the one that should be returned - // If a primitive should be returned we check it's wrapper type - val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor) - if (expectedType != hookMethodReturnType) { - // Cast object - mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType)) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) + // Push a reference to our object array with the saved arguments + mv.visitVarInsn(Opcodes.ALOAD, localObjArr) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) + // Push the hook id + mv.visitLdcInsn(hookId) + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // How we proceed depends on the type of hook we want to implement + when (hook.hookType) { + HookType.BEFORE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + + // Call the original method if this is the last BEFORE hook. If not, the original method will be + // called by the next AFTER hook. + if (index == matchingHooks.lastIndex) { + // Stack layout: ... + // Push the values for the original method call onto the stack again + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object } - // Check if we need to unwrap the returned object - unwrapTypeIfPrimitive(returnTypeDescriptor) + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) } } - } - HookType.AFTER -> { - // Push the values for the original method call again onto the stack - if (opcode != Opcodes.INVOKESTATIC) { - mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object - } - loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... - // Call the original method or the next hook in order - visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface) - val returnTypeDescriptor = extractReturnTypeDescriptor(methodDescriptor) - if (returnTypeDescriptor == "V") { - // If the method didn't return anything, we push a nullref as placeholder - mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + HookType.REPLACE -> { + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + // Stack layout: ... | [return value (primitive/objectref)] + // Check if we need to process the return value + if (returnTypeDescriptor != "V") { + val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor) + // if the hook method's return type is primitive we don't need to unwrap or cast it + if (!isPrimitiveType(hookMethodReturnType)) { + // Check if the returned object type is different than the one that should be returned + // If a primitive should be returned we check it's wrapper type + val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor) + if (expectedType != hookMethodReturnType) { + // Cast object + mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType)) + } + // Check if we need to unwrap the returned object + unwrapTypeIfPrimitive(returnTypeDescriptor) + } + } } - // Wrap return value if it is a primitive type - wrapTypeIfPrimitive(returnTypeDescriptor) - // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) - // | return value (objectref) - // Store the result value in a local variable (but keep it on the stack) - val localReturnObj = lvs.newLocal(Type.getType(getWrapperTypeDescriptor(returnTypeDescriptor))) - mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref - mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref - // Call the hook method - mv.visitMethodInsn( - Opcodes.INVOKESTATIC, - hook.hookInternalClassName, - hook.hookMethodName, - hook.hookMethodDescriptor, - false - ) - // Stack layout: ... - if (returnTypeDescriptor != "V") { - // Push the return value again + HookType.AFTER -> { + // Call the original method before the first AFTER hook + if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) { + // Push the values for the original method call again onto the stack + if (opcode != Opcodes.INVOKESTATIC) { + mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object + } + loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ... + mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface) + if (returnTypeDescriptor == "V") { + // If the method didn't return anything, we push a nullref as placeholder + mv.visitInsn(Opcodes.ACONST_NULL) // push nullref + } + // Wrap return value if it is a primitive type + wrapTypeIfPrimitive(returnTypeDescriptor) + mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref + } mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref - // Unwrap it, if it was a primitive value - unwrapTypeIfPrimitive(returnTypeDescriptor) - // Stack layout: ... | return value (primitive/objectref) + + // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int) + // | return value (objectref) + // Store the result value in a local variable (but keep it on the stack) + // Call the hook method + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + hook.hookInternalClassName, + hook.hookMethodName, + hook.hookMethodDescriptor, + false + ) + // Stack layout: ... + // Push the return value on the stack after the last AFTER hook if the original method returns a value + if (index == matchingHooks.size - 1 && returnTypeDescriptor != "V") { + // Push the return value again + mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref + // Unwrap it, if it was a primitive value + unwrapTypeIfPrimitive(returnTypeDescriptor) + // Stack layout: ... | return value (primitive/objectref) + } } } } @@ -286,10 +271,38 @@ private class HookMethodVisitor( Opcodes.INVOKESPECIAL ) - private fun findMatchingHook(hookType: HookType, owner: String, name: String, descriptor: String): Hook? { - val withoutDescriptorKey = "$hookType#$owner#$name" - val withDescriptorKey = "$withoutDescriptorKey#$descriptor" - return hooks[withDescriptorKey] ?: hooks[withoutDescriptorKey] + private fun findMatchingHooks(owner: String, name: String, descriptor: String): List<Hook> { + val result = HookType.values().flatMap { hookType -> + val withoutDescriptorKey = "$hookType#$owner#$name" + val withDescriptorKey = "$withoutDescriptorKey#$descriptor" + hooks[withDescriptorKey].orEmpty() + hooks[withoutDescriptorKey].orEmpty() + }.sortedBy { it.hookType } + val replaceHookCount = result.count { it.hookType == HookType.REPLACE } + check( + replaceHookCount == 0 || + (replaceHookCount == 1 && result.size == 1) + ) { + "For a given method, You can either have a single REPLACE hook or BEFORE/AFTER hooks. Found:\n $result" + } + + return result + .filter { !isReplaceHookInJava6mode(it) } + .sortedByDescending { it.toString() } + } + + private fun isReplaceHookInJava6mode(hook: Hook): Boolean { + if (java6Mode && hook.hookType == HookType.REPLACE) { + if (showUnsupportedHookWarning.getAndSet(false)) { + println( + """WARN: Some hooks could not be applied to class files built for Java 7 or lower. + |WARN: Ensure that the fuzz target and its dependencies are compiled with + |WARN: -target 8 or higher to identify as many bugs as possible. + """.trimMargin() + ) + } + return true + } + return false } // Stores all arguments for a method call in a local object array. @@ -350,7 +363,7 @@ private class HookMethodVisitor( } // Removes a primitive value from the top of the operand stack - // and pushes it enclosed in it's wrapper type (e.g. removes int, pushes Integer). + // and pushes it enclosed in its wrapper type (e.g. removes int, pushes Integer). // This is done by calling .valueOf(...) on the wrapper class. private fun wrapTypeIfPrimitive(unwrappedTypeDescriptor: String) { if (!isPrimitiveType(unwrappedTypeDescriptor) || unwrappedTypeDescriptor == "V") return diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt new file mode 100644 index 00000000..66a21ee7 --- /dev/null +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Hooks.kt @@ -0,0 +1,114 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor + +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import com.code_intelligence.jazzer.utils.ClassNameGlobber +import com.code_intelligence.jazzer.utils.descriptor +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult +import java.lang.reflect.Method + +data class Hooks( + val hooks: List<Hook>, + val hookClasses: Set<Class<*>>, + val additionalHookClassNameGlobber: ClassNameGlobber +) { + + companion object { + fun loadHooks(vararg hookClassNames: Set<String>): List<Hooks> { + return ClassGraph() + .enableClassInfo() + .enableSystemJarsAndModules() + .rejectPackages("jaz.*", "com.code_intelligence.jazzer.*") + .scan() + .use { scanResult -> + // Capture scanResult in HooksLoader field to not pass it through + // all internal hook loading methods. + val loader = HooksLoader(scanResult) + hookClassNames.map(loader::load) + } + } + + private class HooksLoader(private val scanResult: ScanResult) { + fun load(hookClassNames: Set<String>): Hooks { + val hooksWithHookClasses = hookClassNames.flatMap(::loadHooks) + val hooks = hooksWithHookClasses.map { it.first } + val hookClasses = hooksWithHookClasses.map { it.second }.toSet() + val additionalHookClassNameGlobber = ClassNameGlobber( + hooks.flatMap(Hook::additionalClassesToHook), + emptyList() + ) + return Hooks(hooks, hookClasses, additionalHookClassNameGlobber) + } + + private fun loadHooks(hookClassName: String): List<Pair<Hook, Class<*>>> { + return try { + // Custom hook classes outside the agent jar can not be found by bootstrap + // class loader, so use the system class loader as that will be the main application + // class loader and can access jars on the classpath. + // 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, ClassLoader.getSystemClassLoader()) + loadHooks(hookClass).also { + println("INFO: Loaded ${it.size} hooks from $hookClassName") + }.map { + it to hookClass + } + } catch (e: ClassNotFoundException) { + println("WARN: Failed to load hooks from $hookClassName: ${e.printStackTrace()}") + emptyList() + } + } + + private fun loadHooks(hookClass: Class<*>): List<Hook> { + val hooks = mutableListOf<Hook>() + for (method in hookClass.methods.sortedBy { it.descriptor }) { + method.getAnnotation(MethodHook::class.java)?.let { + hooks.addAll(verifyAndGetHooks(method, it)) + } + method.getAnnotation(MethodHooks::class.java)?.let { + it.value.forEach { hookAnnotation -> + hooks.addAll(verifyAndGetHooks(method, hookAnnotation)) + } + } + } + return hooks + } + + private fun verifyAndGetHooks(hookMethod: Method, hookData: MethodHook): List<Hook> { + return lookupClassesToHook(hookData.targetClassName) + .map { className -> + Hook.createAndVerifyHook(hookMethod, hookData, className) + } + } + + private fun lookupClassesToHook(annotationTargetClassName: String): List<String> { + // Allowing arbitrary exterior whitespace in the target class name allows for an easy workaround + // for mangled hooks due to shading applied to hooks. + val targetClassName = annotationTargetClassName.trim() + val targetClassInfo = scanResult.getClassInfo(targetClassName) ?: return listOf(targetClassName) + val additionalTargetClasses = when { + targetClassInfo.isInterface -> scanResult.getClassesImplementing(targetClassName) + targetClassInfo.isAbstract -> scanResult.getSubclasses(targetClassName) + else -> emptyList() + } + return (listOf(targetClassName) + additionalTargetClasses.map { it.name }).sorted() + } + } + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt index 86ad45a3..78793842 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/Instrumentor.kt @@ -14,8 +14,8 @@ package com.code_intelligence.jazzer.instrumentor -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.MethodNode enum class InstrumentationType { CMP, diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java new file mode 100644 index 00000000..0512ec2a --- /dev/null +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/StaticMethodStrategy.java @@ -0,0 +1,48 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.instrumentor; + +import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.InstrSupport; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class StaticMethodStrategy implements EdgeCoverageStrategy { + @Override + public void instrumentControlFlowEdge( + MethodVisitor mv, int edgeId, int variable, String coverageMapInternalClassName) { + InstrSupport.push(mv, edgeId); + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, coverageMapInternalClassName, "recordCoverage", "(I)V", false); + } + + @Override + public int getInstrumentControlFlowEdgeStackSize() { + return 1; + } + + @Override + public Object getLocalVariableType() { + return null; + } + + @Override + public void loadLocalVariable( + MethodVisitor mv, int variable, String coverageMapInternalClassName) {} + + @Override + public int getLoadLocalVariableStackSize() { + return 0; + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt index e6d3176e..65f11e52 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentor.kt @@ -15,19 +15,19 @@ package com.code_intelligence.jazzer.instrumentor import com.code_intelligence.jazzer.runtime.TraceDataFlowNativeCallbacks -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassReader -import com.code_intelligence.jazzer.third_party.objectweb.asm.ClassWriter -import com.code_intelligence.jazzer.third_party.objectweb.asm.Opcodes -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.AbstractInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.ClassNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnList -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.InsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.IntInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LdcInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.LookupSwitchInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodInsnNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.MethodNode -import com.code_intelligence.jazzer.third_party.objectweb.asm.tree.TableSwitchInsnNode +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.LookupSwitchInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.TableSwitchInsnNode internal class TraceDataFlowInstrumentor(private val types: Set<InstrumentationType>, callbackClass: Class<*> = TraceDataFlowNativeCallbacks::class.java) : Instrumentor { @@ -133,7 +133,7 @@ internal class TraceDataFlowInstrumentor(private val types: Set<InstrumentationT } private fun InsnList.pushFakePc() { - add(LdcInsnNode(random.nextInt(4096))) + add(LdcInsnNode(random.nextInt(512))) } private fun longCmpInstrumentation() = InsnList().apply { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules b/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules deleted file mode 100644 index c2092b3b..00000000 --- a/agent/src/main/java/com/code_intelligence/jazzer/instrumentor/shade_rules +++ /dev/null @@ -1 +0,0 @@ -rule org.** com.code_intelligence.jazzer.third_party.@1
\ No newline at end of file diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel index df28adb4..08bd7653 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/BUILD.bazel @@ -3,8 +3,7 @@ load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") java_jni_library( name = "replay", srcs = ["Replayer.java"], - native_libs = ["//agent/src/main/native/com/code_intelligence/jazzer/replay"], - visibility = ["//agent/src/main/native/com/code_intelligence/jazzer/replay:__pkg__"], + native_libs = ["//driver/src/main/native/com/code_intelligence/jazzer/driver:fuzzed_data_provider_standalone"], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/api", "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", diff --git a/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java index fc6bfc4f..0a250d1a 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/replay/Replayer.java @@ -29,8 +29,10 @@ public class Replayer { public static final int STATUS_OTHER_ERROR = 1; static { + System.setProperty("jazzer.is_replayer", "true"); try { - RulesJni.loadLibrary("replay", Replayer.class); + RulesJni.loadLibrary( + "fuzzed_data_provider_standalone", "/com/code_intelligence/jazzer/driver"); } catch (Throwable t) { t.printStackTrace(); System.exit(STATUS_OTHER_ERROR); @@ -104,7 +106,9 @@ public class Replayer { try { Method fuzzerTestOneInput = fuzzTarget.getMethod("fuzzerTestOneInput", FuzzedDataProvider.class); - fuzzerTestOneInput.invoke(null, makeFuzzedDataProvider(input)); + try (FuzzedDataProviderImpl fuzzedDataProvider = FuzzedDataProviderImpl.withJavaData(input)) { + fuzzerTestOneInput.invoke(null, fuzzedDataProvider); + } return; } catch (Exception e) { handleInvokeException(e, fuzzTarget); @@ -149,11 +153,4 @@ public class Replayer { } } } - - private static FuzzedDataProvider makeFuzzedDataProvider(byte[] input) { - feedFuzzedDataProvider(input); - return new FuzzedDataProviderImpl(); - } - - private static native void feedFuzzedDataProvider(byte[] input); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel index 095b0bf8..0d8162d5 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -1,47 +1,87 @@ -load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") -java_library( +java_jni_library( name = "fuzzed_data_provider", srcs = [ "FuzzedDataProviderImpl.java", ], - visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"], + visibility = [ + "//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__", + "//agent/src/test/java/com/code_intelligence/jazzer/runtime:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], deps = [ + ":unsafe_provider", "//agent/src/main/java/com/code_intelligence/jazzer/api", ], ) -java_library( +java_jni_library( + name = "coverage_map", + srcs = ["CoverageMap.java"], + visibility = [ + "//agent/src/jmh/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/test:__subpackages__", + ], + deps = [ + ":unsafe_provider", + ], +) + +java_jni_library( name = "signal_handler", srcs = ["SignalHandler.java"], - javacopts = [ - "-XDenableSunApiLintControl", + native_libs = ["//agent/src/main/native/com/code_intelligence/jazzer/runtime:jazzer_signal_handler"], + visibility = [ + "//agent/src/main/native/com/code_intelligence/jazzer/runtime:__pkg__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", ], ) -kt_jvm_library( +java_jni_library( + name = "trace_data_flow_native_callbacks", + srcs = ["TraceDataFlowNativeCallbacks.java"], + visibility = [ + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/utils", + ], +) + +java_library( + name = "unsafe_provider", + srcs = ["UnsafeProvider.java"], + visibility = [ + "//driver/src:__subpackages__", + "//sanitizers/src/main/java:__subpackages__", + ], +) + +java_library( name = "runtime", srcs = [ - "CoverageMap.java", - "ExceptionUtils.kt", "HardToCatchError.java", "JazzerInternal.java", - "ManifestUtils.kt", "NativeLibHooks.java", "RecordingFuzzedDataProvider.java", - "SignalHandler.java", "TraceCmpHooks.java", - "TraceDataFlowNativeCallbacks.java", "TraceDivHooks.java", "TraceIndirHooks.java", ], visibility = ["//visibility:public"], runtime_deps = [ + ":signal_handler", "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", ], deps = [ + ":coverage_map", ":fuzzed_data_provider", - ":signal_handler", + ":trace_data_flow_native_callbacks", "//agent/src/main/java/com/code_intelligence/jazzer/api", "//agent/src/main/java/com/code_intelligence/jazzer/utils", ], diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java index af2424a2..4069d25a 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/CoverageMap.java @@ -14,20 +14,116 @@ package com.code_intelligence.jazzer.runtime; -import java.nio.ByteBuffer; +import com.github.fmeum.rules_jni.RulesJni; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import sun.misc.Unsafe; /** - * Represents the Java view on a libFuzzer 8 bit counter coverage map. - * By using a direct ByteBuffer, the counter array is shared directly with - * native code. + * Represents the Java view on a libFuzzer 8 bit counter coverage map. By using a direct ByteBuffer, + * the counters are shared directly with native code. */ final public class CoverageMap { - public static ByteBuffer mem = ByteBuffer.allocateDirect(0); + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + + private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS"; + + private static final int MAX_NUM_COUNTERS = System.getenv(ENV_MAX_NUM_COUNTERS) != null + ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS)) + : 1 << 20; - public static void enlargeCoverageMap() { - registerNewCoverageCounters(); - System.out.println("INFO: New number of inline 8-bit counters: " + mem.capacity()); + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + + static { + if (UNSAFE == null) { + System.out.println("ERROR: Failed to get Unsafe instance for CoverageMap.%n" + + " Please file a bug at:%n" + + " https://github.com/CodeIntelligenceTesting/jazzer/issues/new"); + System.exit(1); + } } - private static native void registerNewCoverageCounters(); + /** + * The collection of coverage counters directly interacted with by classes that are instrumented + * for coverage. The instrumentation assumes that this is always one contiguous block of memory, + * so it is allocated once at maximum size. Using a larger number here increases the memory usage + * of all fuzz targets, but has otherwise no impact on performance. + */ + public static final long countersAddress = UNSAFE.allocateMemory(MAX_NUM_COUNTERS); + + static { + UNSAFE.setMemory(countersAddress, MAX_NUM_COUNTERS, (byte) 0); + initialize(countersAddress); + } + + private static final int INITIAL_NUM_COUNTERS = 1 << 9; + + static { + registerNewCounters(0, INITIAL_NUM_COUNTERS); + } + + /** + * The number of coverage counters that are currently registered with libFuzzer. This number grows + * dynamically as classes are instrumented and should be kept as low as possible as libFuzzer has + * to iterate over the whole map for every execution. + */ + private static int currentNumCounters = INITIAL_NUM_COUNTERS; + + // Called via reflection. + @SuppressWarnings("unused") + public static void enlargeIfNeeded(int nextId) { + int newNumCounters = currentNumCounters; + while (nextId >= newNumCounters) { + newNumCounters = 2 * newNumCounters; + if (newNumCounters > MAX_NUM_COUNTERS) { + System.out.printf("ERROR: Maximum number (%s) of coverage counters exceeded. Try to%n" + + " limit the scope of a single fuzz target as much as possible to keep the%n" + + " fuzzer fast.%n" + + " If that is not possible, the maximum number of counters can be increased%n" + + " via the %s environment variable.", + MAX_NUM_COUNTERS, ENV_MAX_NUM_COUNTERS); + System.exit(1); + } + } + if (newNumCounters > currentNumCounters) { + registerNewCounters(currentNumCounters, newNumCounters); + currentNumCounters = newNumCounters; + System.out.println("INFO: New number of coverage counters: " + currentNumCounters); + } + } + + // Called by the coverage instrumentation. + @SuppressWarnings("unused") + public static void recordCoverage(final int id) { + final long address = countersAddress + id; + final byte counter = UNSAFE.getByte(address); + UNSAFE.putByte(address, (byte) (counter == -1 ? 1 : counter + 1)); + } + + public static Set<Integer> getCoveredIds() { + Set<Integer> coveredIds = new HashSet<>(); + for (int id = 0; id < currentNumCounters; id++) { + if (UNSAFE.getByte(countersAddress + id) > 0) { + coveredIds.add(id); + } + } + return Collections.unmodifiableSet(coveredIds); + } + + public static void replayCoveredIds(Set<Integer> coveredIds) { + for (int id : coveredIds) { + UNSAFE.putByte(countersAddress + id, (byte) 1); + } + } + + // Returns the IDs of all blocks that have been covered in at least one run (not just the current + // one). + public static native int[] getEverCoveredIds(); + + private static native void initialize(long countersAddress); + + private static native void registerNewCounters(int oldNumCounters, int newNumCounters); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java index fe4d8ac7..b7aad33e 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl.java @@ -15,9 +15,119 @@ package com.code_intelligence.jazzer.runtime; import com.code_intelligence.jazzer.api.FuzzedDataProvider; - -public class FuzzedDataProviderImpl implements FuzzedDataProvider { - public FuzzedDataProviderImpl() {} +import com.github.fmeum.rules_jni.RulesJni; +import sun.misc.Unsafe; + +public class FuzzedDataProviderImpl implements FuzzedDataProvider, AutoCloseable { + static { + // The replayer loads a standalone version of the FuzzedDataProvider. + if (System.getProperty("jazzer.is_replayer") == null) { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } + nativeInit(); + } + + private static native void nativeInit(); + + private final boolean ownsNativeData; + private long originalDataPtr; + private int originalRemainingBytes; + + // Accessed in fuzzed_data_provider.cpp. + private long dataPtr; + private int remainingBytes; + + private FuzzedDataProviderImpl(long dataPtr, int remainingBytes, boolean ownsNativeData) { + this.ownsNativeData = ownsNativeData; + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = remainingBytes; + this.remainingBytes = remainingBytes; + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <ul> + * <li>{@link #close()} <b>must</b> be called on instances created with this method to free the + * native copy of the Java + * {@code byte} array. + * <li>{@link #setNativeData(long, int)} <b>must not</b> be called on instances created with this + * method. + * + * @param data the raw bytes used as input + * @return a {@link FuzzedDataProvider} backed by {@code data} + */ + public static FuzzedDataProviderImpl withJavaData(byte[] data) { + return new FuzzedDataProviderImpl(allocateNativeCopy(data), data.length, true); + } + + /** + * Creates a {@link FuzzedDataProvider} that consumes bytes from an already existing native array. + * + * <p>The backing array can be set at any time using {@link #setNativeData(long, int)} and is + * initially empty. + * + * @return a {@link FuzzedDataProvider} backed by an empty array. + */ + public static FuzzedDataProviderImpl withNativeData() { + return new FuzzedDataProviderImpl(0, 0, false); + } + + /** + * Replaces the current native backing array. + * + * <p><b>Must not</b> be called on instances created with {@link #withJavaData(byte[])}. + * + * @param dataPtr a native pointer to the new backing array + * @param dataLength the length of the new backing array + */ + public void setNativeData(long dataPtr, int dataLength) { + this.originalDataPtr = dataPtr; + this.dataPtr = dataPtr; + this.originalRemainingBytes = dataLength; + this.remainingBytes = dataLength; + } + + /** + * Resets the FuzzedDataProvider state to read from the beginning to the end of its current + * backing item. + */ + public void reset() { + dataPtr = originalDataPtr; + remainingBytes = originalRemainingBytes; + } + + /** + * Releases native memory allocated for this instance (if any). + * + * <p>While the instance should not be used after this method returns, no usage of {@link + * FuzzedDataProvider} methods can result in memory corruption. + */ + @Override + public void close() { + if (originalDataPtr == 0) { + return; + } + if (ownsNativeData) { + UNSAFE.freeMemory(originalDataPtr); + } + // Prevent double-frees and use-after-frees by effectively making all methods no-ops after + // close() has been called. + originalDataPtr = 0; + originalRemainingBytes = 0; + dataPtr = 0; + remainingBytes = 0; + } + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + private static long allocateNativeCopy(byte[] data) { + long nativeCopy = UNSAFE.allocateMemory(data.length); + UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, nativeCopy, data.length); + return nativeCopy; + } @Override public native boolean consumeBoolean(); @@ -25,23 +135,51 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native byte consumeByte(); - @Override public native byte consumeByte(byte min, byte max); + @Override + public byte consumeByte(byte min, byte max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeByteUnchecked(min, max); + } @Override public native short consumeShort(); - @Override public native short consumeShort(short min, short max); + @Override + public short consumeShort(short min, short max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeShortUnchecked(min, max); + } @Override public native short[] consumeShorts(int maxLength); @Override public native int consumeInt(); - @Override public native int consumeInt(int min, int max); + @Override + public int consumeInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeIntUnchecked(min, max); + } @Override public native int[] consumeInts(int maxLength); @Override public native long consumeLong(); - @Override public native long consumeLong(long min, long max); + @Override + public long consumeLong(long min, long max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %d, max: %d)", min, max)); + } + return consumeLongUnchecked(min, max); + } @Override public native long[] consumeLongs(int maxLength); @@ -49,13 +187,27 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native float consumeRegularFloat(); - @Override public native float consumeRegularFloat(float min, float max); + @Override + public float consumeRegularFloat(float min, float max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularFloatUnchecked(min, max); + } @Override public native float consumeProbabilityFloat(); @Override public native double consumeDouble(); - @Override public native double consumeRegularDouble(double min, double max); + @Override + public double consumeRegularDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %f, max: %f)", min, max)); + } + return consumeRegularDoubleUnchecked(min, max); + } @Override public native double consumeRegularDouble(); @@ -63,7 +215,14 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native char consumeChar(); - @Override public native char consumeChar(char min, char max); + @Override + public char consumeChar(char min, char max) { + if (min > max) { + throw new IllegalArgumentException( + String.format("min must be <= max (got min: %c, max: %c)", min, max)); + } + return consumeCharUnchecked(min, max); + } @Override public native char consumeCharNoSurrogates(); @@ -80,4 +239,12 @@ public class FuzzedDataProviderImpl implements FuzzedDataProvider { @Override public native byte[] consumeRemainingAsBytes(); @Override public native int remainingBytes(); + + private native byte consumeByteUnchecked(byte min, byte max); + private native short consumeShortUnchecked(short min, short max); + private native char consumeCharUnchecked(char min, char max); + private native int consumeIntUnchecked(int min, int max); + private native long consumeLongUnchecked(long min, long max); + private native float consumeRegularFloatUnchecked(float min, float max); + private native double consumeRegularDoubleUnchecked(double min, double max); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java index 8bc1b38c..79c851ad 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/JazzerInternal.java @@ -14,9 +14,12 @@ package com.code_intelligence.jazzer.runtime; +import java.util.ArrayList; + final public class JazzerInternal { - // Accessed from native code. - private static Throwable lastFinding; + private static final ArrayList<Runnable> ON_FUZZ_TARGET_READY_CALLBACKS = new ArrayList<>(); + + public static Throwable lastFinding; // Accessed from api.Jazzer via reflection. public static void reportFindingFromHook(Throwable finding) { @@ -26,4 +29,13 @@ final public class JazzerInternal { // target returns even if this Error is swallowed. throw new HardToCatchError(); } + + public static void registerOnFuzzTargetReadyCallback(Runnable callback) { + ON_FUZZ_TARGET_READY_CALLBACKS.add(callback); + } + + public static void onFuzzTargetReady(String fuzzTargetClass) { + ON_FUZZ_TARGET_READY_CALLBACKS.forEach(Runnable::run); + ON_FUZZ_TARGET_READY_CALLBACKS.clear(); + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java index 976e024c..4eb80222 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider.java @@ -18,49 +18,33 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Base64; // Wraps the native FuzzedDataProviderImpl and serializes all its return values // into a Base64-encoded string. -final class RecordingFuzzedDataProvider implements InvocationHandler { - private final FuzzedDataProvider target = new FuzzedDataProviderImpl(); +public final class RecordingFuzzedDataProvider implements FuzzedDataProvider { + private final FuzzedDataProvider target; private final ArrayList<Object> recordedReplies = new ArrayList<>(); - private RecordingFuzzedDataProvider() {} + private RecordingFuzzedDataProvider(FuzzedDataProvider target) { + this.target = target; + } - // Called from native code. - public static FuzzedDataProvider makeFuzzedDataProviderProxy() { - return (FuzzedDataProvider) Proxy.newProxyInstance( - RecordingFuzzedDataProvider.class.getClassLoader(), new Class[] {FuzzedDataProvider.class}, - new RecordingFuzzedDataProvider()); + public static FuzzedDataProvider makeFuzzedDataProviderProxy(FuzzedDataProvider target) { + return new RecordingFuzzedDataProvider(target); } - // Called from native code. public static String serializeFuzzedDataProviderProxy(FuzzedDataProvider proxy) throws IOException { - return ((RecordingFuzzedDataProvider) Proxy.getInvocationHandler(proxy)).serialize(); + return ((RecordingFuzzedDataProvider) proxy).serialize(); } - private Object recordAndReturn(Object object) { + private <T> T recordAndReturn(T object) { recordedReplies.add(object); return object; } - @Override - public Object invoke(Object object, Method method, Object[] args) throws Throwable { - if (method.isDefault()) { - // Default methods in FuzzedDataProvider are implemented in Java and - // don't need to be recorded. - return method.invoke(target, args); - } else { - return recordAndReturn(method.invoke(target, args)); - } - } - private String serialize() throws IOException { byte[] rawOut; try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { @@ -71,4 +55,159 @@ final class RecordingFuzzedDataProvider implements InvocationHandler { } return Base64.getEncoder().encodeToString(rawOut); } + + @Override + public boolean consumeBoolean() { + return recordAndReturn(target.consumeBoolean()); + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return recordAndReturn(target.consumeBooleans(maxLength)); + } + + @Override + public byte consumeByte() { + return recordAndReturn(target.consumeByte()); + } + + @Override + public byte consumeByte(byte min, byte max) { + return recordAndReturn(target.consumeByte(min, max)); + } + + @Override + public byte[] consumeBytes(int maxLength) { + return recordAndReturn(target.consumeBytes(maxLength)); + } + + @Override + public byte[] consumeRemainingAsBytes() { + return recordAndReturn(target.consumeRemainingAsBytes()); + } + + @Override + public short consumeShort() { + return recordAndReturn(target.consumeShort()); + } + + @Override + public short consumeShort(short min, short max) { + return recordAndReturn(target.consumeShort(min, max)); + } + + @Override + public short[] consumeShorts(int maxLength) { + return recordAndReturn(target.consumeShorts(maxLength)); + } + + @Override + public int consumeInt() { + return recordAndReturn(target.consumeInt()); + } + + @Override + public int consumeInt(int min, int max) { + return recordAndReturn(target.consumeInt(min, max)); + } + + @Override + public int[] consumeInts(int maxLength) { + return recordAndReturn(target.consumeInts(maxLength)); + } + + @Override + public long consumeLong() { + return recordAndReturn(target.consumeLong()); + } + + @Override + public long consumeLong(long min, long max) { + return recordAndReturn(target.consumeLong(min, max)); + } + + @Override + public long[] consumeLongs(int maxLength) { + return recordAndReturn(target.consumeLongs(maxLength)); + } + + @Override + public float consumeFloat() { + return recordAndReturn(target.consumeFloat()); + } + + @Override + public float consumeRegularFloat() { + return recordAndReturn(target.consumeRegularFloat()); + } + + @Override + public float consumeRegularFloat(float min, float max) { + return recordAndReturn(target.consumeRegularFloat(min, max)); + } + + @Override + public float consumeProbabilityFloat() { + return recordAndReturn(target.consumeProbabilityFloat()); + } + + @Override + public double consumeDouble() { + return recordAndReturn(target.consumeDouble()); + } + + @Override + public double consumeRegularDouble() { + return recordAndReturn(target.consumeRegularDouble()); + } + + @Override + public double consumeRegularDouble(double min, double max) { + return recordAndReturn(target.consumeRegularDouble(min, max)); + } + + @Override + public double consumeProbabilityDouble() { + return recordAndReturn(target.consumeProbabilityDouble()); + } + + @Override + public char consumeChar() { + return recordAndReturn(target.consumeChar()); + } + + @Override + public char consumeChar(char min, char max) { + return recordAndReturn(target.consumeChar(min, max)); + } + + @Override + public char consumeCharNoSurrogates() { + return recordAndReturn(target.consumeCharNoSurrogates()); + } + + @Override + public String consumeString(int maxLength) { + return recordAndReturn(target.consumeString(maxLength)); + } + + @Override + public String consumeRemainingAsString() { + return recordAndReturn(target.consumeRemainingAsString()); + } + + @Override + public String consumeAsciiString(int maxLength) { + return recordAndReturn(target.consumeAsciiString(maxLength)); + } + + @Override + public String consumeRemainingAsAsciiString() { + return recordAndReturn(target.consumeRemainingAsAsciiString()); + } + + @Override + public int remainingBytes() { + return recordAndReturn(target.remainingBytes()); + } } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java index 0a42aa94..49ee80c8 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/SignalHandler.java @@ -14,13 +14,18 @@ package com.code_intelligence.jazzer.runtime; +import com.github.fmeum.rules_jni.RulesJni; import sun.misc.Signal; -@SuppressWarnings({"unused", "sunapi"}) -final class SignalHandler { - public static native void handleInterrupt(); - - public static void setupSignalHandlers() { +public final class SignalHandler { + static { + RulesJni.loadLibrary("jazzer_signal_handler", SignalHandler.class); Signal.handle(new Signal("INT"), sig -> handleInterrupt()); } + + public static void initialize() { + // Implicitly runs the static initializer. + } + + private static native void handleInterrupt(); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java index 352da8ea..37e8eaeb 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceCmpHooks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; import java.util.Arrays; +import java.util.ConcurrentModificationException; import java.util.Map; import java.util.TreeMap; @@ -80,6 +81,18 @@ final public class TraceCmpHooks { } } + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Object", targetMethod = "equals") + @MethodHook( + type = HookType.AFTER, targetClassName = "java.lang.CharSequence", targetMethod = "equals") + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.Number", targetMethod = "equals") + public static void + genericEquals( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (!returnValue && arguments[0] != null && thisObject.getClass() == arguments[0].getClass()) { + TraceDataFlowNativeCallbacks.traceGenericCmp(thisObject, arguments[0], hookId); + } + } + @MethodHook( type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "compareTo") @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", @@ -193,9 +206,9 @@ final public class TraceCmpHooks { replace( MethodHandle method, Object thisObject, Object[] arguments, int hookId, String returnValue) { String original = (String) thisObject; - String target = arguments[0].toString(); // Report only if the replacement was not successful. if (original.equals(returnValue)) { + String target = arguments[0].toString(); TraceDataFlowNativeCallbacks.traceStrstr(original, target, hookId); } } @@ -205,11 +218,11 @@ final public class TraceCmpHooks { public static void arraysEquals( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; byte[] first = (byte[]) arguments[0]; byte[] second = (byte[]) arguments[1]; - if (!returnValue) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "equals", @@ -217,13 +230,13 @@ final public class TraceCmpHooks { public static void arraysEqualsRange( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Boolean returnValue) { + if (returnValue) + return; byte[] first = Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); byte[] second = Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); - if (!returnValue) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, 1, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", @@ -233,11 +246,11 @@ final public class TraceCmpHooks { public static void arraysCompare( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; byte[] first = (byte[]) arguments[0]; byte[] second = (byte[]) arguments[1]; - if (returnValue != 0) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); } @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Arrays", targetMethod = "compare", @@ -247,34 +260,22 @@ final public class TraceCmpHooks { public static void arraysCompareRange( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Integer returnValue) { + if (returnValue == 0) + return; byte[] first = Arrays.copyOfRange((byte[]) arguments[0], (int) arguments[1], (int) arguments[2]); byte[] second = Arrays.copyOfRange((byte[]) arguments[3], (int) arguments[4], (int) arguments[5]); - if (returnValue != 0) { - TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); - } + TraceDataFlowNativeCallbacks.traceMemcmp(first, second, returnValue, hookId); } // The maximal number of elements of a non-TreeMap Map that will be sorted and searched for the // key closest to the current lookup key in the mapGet hook. private static final int MAX_NUM_KEYS_TO_ENUMERATE = 100; - @MethodHook(type = HookType.AFTER, targetClassName = "com.google.common.collect.ImmutableMap", - targetMethod = "get") - @MethodHook( - type = HookType.AFTER, targetClassName = "java.util.AbstractMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.EnumMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.HashMap", targetMethod = "get") - @MethodHook( - type = HookType.AFTER, targetClassName = "java.util.LinkedHashMap", targetMethod = "get") + @SuppressWarnings({"rawtypes", "unchecked"}) @MethodHook(type = HookType.AFTER, targetClassName = "java.util.Map", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.SortedMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.TreeMap", targetMethod = "get") - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.concurrent.ConcurrentMap", - targetMethod = "get") - public static void - mapGet( + public static void mapGet( MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { if (returnValue != null) return; @@ -291,31 +292,47 @@ final public class TraceCmpHooks { // https://github.com/llvm/llvm-project/blob/318942de229beb3b2587df09e776a50327b5cef0/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L564 Object lowerBoundKey = null; Object upperBoundKey = null; - if (map instanceof TreeMap) { - final TreeMap treeMap = (TreeMap) map; - lowerBoundKey = treeMap.floorKey(currentKey); - upperBoundKey = treeMap.ceilingKey(currentKey); - } else if (currentKey instanceof Comparable) { - final Comparable comparableKey = (Comparable) currentKey; - // Find two keys that bracket currentKey. - // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE. - int enumeratedKeys = 0; - for (Object validKey : map.keySet()) { - if (validKey == null) - continue; - // If the key sorts lower than the non-existing key, but higher than the current lower - // bound, update the lower bound and vice versa for the upper bound. - if (comparableKey.compareTo(validKey) > 0 - && (lowerBoundKey == null || ((Comparable) validKey).compareTo(lowerBoundKey) > 0)) { - lowerBoundKey = validKey; + try { + if (map instanceof TreeMap) { + final TreeMap treeMap = (TreeMap) map; + try { + lowerBoundKey = treeMap.floorKey(currentKey); + upperBoundKey = treeMap.ceilingKey(currentKey); + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. } - if (comparableKey.compareTo(validKey) < 0 - && (upperBoundKey == null || ((Comparable) validKey).compareTo(upperBoundKey) < 0)) { - upperBoundKey = validKey; + } else if (currentKey instanceof Comparable) { + final Comparable comparableCurrentKey = (Comparable) currentKey; + // Find two keys that bracket currentKey. + // Note: This is not deterministic if map.size() > MAX_NUM_KEYS_TO_ENUMERATE. + int enumeratedKeys = 0; + for (Object validKey : map.keySet()) { + if (!(validKey instanceof Comparable)) + continue; + final Comparable comparableValidKey = (Comparable) validKey; + // If the key sorts lower than the non-existing key, but higher than the current lower + // bound, update the lower bound and vice versa for the upper bound. + try { + if (comparableValidKey.compareTo(comparableCurrentKey) < 0 + && (lowerBoundKey == null || comparableValidKey.compareTo(lowerBoundKey) > 0)) { + lowerBoundKey = validKey; + } + if (comparableValidKey.compareTo(comparableCurrentKey) > 0 + && (upperBoundKey == null || comparableValidKey.compareTo(upperBoundKey) < 0)) { + upperBoundKey = validKey; + } + } catch (ClassCastException ignored) { + // Can be thrown by floorKey and ceilingKey if currentKey is of a type that can't be + // compared to the maps keys. + } + if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE) + break; } - if (enumeratedKeys++ > MAX_NUM_KEYS_TO_ENUMERATE) - break; } + } catch (ConcurrentModificationException ignored) { + // map was modified by another thread, skip this invocation + return; } // Modify the hook ID so that compares against distinct valid keys are traced separately. if (lowerBoundKey != null) { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java index 456d0cb9..821ade0d 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/TraceDataFlowNativeCallbacks.java @@ -15,49 +15,32 @@ package com.code_intelligence.jazzer.runtime; import com.code_intelligence.jazzer.utils.Utils; +import com.github.fmeum.rules_jni.RulesJni; import java.lang.reflect.Executable; +import java.nio.charset.Charset; @SuppressWarnings("unused") final public class TraceDataFlowNativeCallbacks { - /* trace-cmp */ - // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceCmpInt(int arg1, int arg2, int pc); - - // Calls: void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceConstCmpInt(int arg1, int arg2, int pc); - - // Calls: void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2); - public static native void traceCmpLong(long arg1, long arg2, int pc); + static { + RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver"); + } - // Calls: void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases); - public static native void traceSwitch(long val, long[] cases, int pc); + // Note that we are not encoding as modified UTF-8 here: The FuzzedDataProvider transparently + // converts CESU8 into modified UTF-8 by coding null bytes on two bytes. Since the fuzzer is more + // likely to insert literal null bytes, having both the fuzzer input and the reported string + // comparisons be CESU8 should perform even better than the current implementation using modified + // UTF-8. + private static final Charset FUZZED_DATA_CHARSET = Charset.forName("CESU8"); - // Calls: void __sanitizer_weak_hook_memcmp(void *caller_pc, const void *b1, const void *b2, - // size_t n, int result); public static native void traceMemcmp(byte[] b1, byte[] b2, int result, int pc); - // Calls: void __sanitizer_weak_hook_strcmp(void *called_pc, const char *s1, const char *s2, int - // result); - public static native void traceStrcmp(String s1, String s2, int result, int pc); - - // Calls: void __sanitizer_weak_hook_strstr(void *called_pc, const char *s1, const char *s2, char - // *result); - public static native void traceStrstr(String s1, String s2, int pc); - - /* trace-div */ - // Calls: void __sanitizer_cov_trace_div4(uint32_t Val); - public static native void traceDivInt(int val, int pc); - - // Calls: void __sanitizer_cov_trace_div8(uint64_t Val); - public static native void traceDivLong(long val, int pc); - - /* trace-gep */ - // Calls: void __sanitizer_cov_trace_gep(uintptr_t Idx); - public static native void traceGep(long val, int pc); + public static void traceStrcmp(String s1, String s2, int result, int pc) { + traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, pc); + } - /* indirect-calls */ - // Calls: void __sanitizer_cov_trace_pc_indir(uintptr_t Callee); - private static native void tracePcIndir(int callee, int caller); + public static void traceStrstr(String s1, String s2, int pc) { + traceStrstr0(encodeForLibFuzzer(s2), pc); + } public static void traceReflectiveCall(Executable callee, int pc) { String className = callee.getDeclaringClass().getCanonicalName(); @@ -75,17 +58,45 @@ final public class TraceDataFlowNativeCallbacks { // The caller has to ensure that arg1 and arg2 have the same class. public static void traceGenericCmp(Object arg1, Object arg2, int pc) { - if (arg1 instanceof String) { - traceStrcmp((String) arg1, (String) arg2, 1, pc); - } else if (arg1 instanceof Integer || arg1 instanceof Short || arg1 instanceof Byte - || arg1 instanceof Character) { + if (arg1 instanceof CharSequence) { + traceStrcmp(arg1.toString(), arg2.toString(), 1, pc); + } else if (arg1 instanceof Integer) { traceCmpInt((int) arg1, (int) arg2, pc); } else if (arg1 instanceof Long) { traceCmpLong((long) arg1, (long) arg2, pc); + } else if (arg1 instanceof Short) { + traceCmpInt((short) arg1, (short) arg2, pc); + } else if (arg1 instanceof Byte) { + traceCmpInt((byte) arg1, (byte) arg2, pc); + } else if (arg1 instanceof Character) { + traceCmpInt((char) arg1, (char) arg2, pc); + } else if (arg1 instanceof Number) { + traceCmpLong(((Number) arg1).longValue(), ((Number) arg2).longValue(), pc); } else if (arg1 instanceof byte[]) { traceMemcmp((byte[]) arg1, (byte[]) arg2, 1, pc); } } + /* trace-cmp */ + public static native void traceCmpInt(int arg1, int arg2, int pc); + public static native void traceConstCmpInt(int arg1, int arg2, int pc); + public static native void traceCmpLong(long arg1, long arg2, int pc); + public static native void traceSwitch(long val, long[] cases, int pc); + /* trace-div */ + public static native void traceDivInt(int val, int pc); + public static native void traceDivLong(long val, int pc); + /* trace-gep */ + public static native void traceGep(long val, int pc); + /* indirect-calls */ + public static native void tracePcIndir(int callee, int caller); + public static native void handleLibraryLoad(); + + private static byte[] encodeForLibFuzzer(String str) { + // libFuzzer string hooks only ever consume the first 64 bytes, so we can definitely cut the + // string off after 64 characters. + return str.substring(0, Math.min(str.length(), 64)).getBytes(FUZZED_DATA_CHARSET); + } + + private static native void traceStrstr0(byte[] needle, int pc); } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java b/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java new file mode 100644 index 00000000..81f2a208 --- /dev/null +++ b/agent/src/main/java/com/code_intelligence/jazzer/runtime/UnsafeProvider.java @@ -0,0 +1,50 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class UnsafeProvider { + private static final Unsafe UNSAFE = getUnsafeInternal(); + + public static Unsafe getUnsafe() { + return UNSAFE; + } + + private static Unsafe getUnsafeInternal() { + try { + // The Java agent is loaded by the bootstrap class loader and should thus + // pass the security checks in getUnsafe. + return Unsafe.getUnsafe(); + } catch (Throwable unused) { + // If not running as an agent, use the classical reflection trick to get an Unsafe instance, + // taking into account that the private field may have a name other than "theUnsafe": + // https://android.googlesource.com/platform/libcore/+/gingerbread/luni/src/main/java/sun/misc/Unsafe.java#32 + try { + for (Field f : Unsafe.class.getDeclaredFields()) { + if (f.getType() == Unsafe.class) { + f.setAccessible(true); + return (Unsafe) f.get(null); + } + } + return null; + } catch (Throwable t) { + t.printStackTrace(); + return null; + } + } + } +} diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel index 5e301efc..10e3477c 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/BUILD.bazel @@ -4,7 +4,12 @@ kt_jvm_library( name = "utils", srcs = [ "ClassNameGlobber.kt", + "ExceptionUtils.kt", + "ManifestUtils.kt", "Utils.kt", ], visibility = ["//visibility:public"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + ], ) diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt index 1f09afe3..44249c81 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ClassNameGlobber.kt @@ -14,28 +14,41 @@ package com.code_intelligence.jazzer.utils -import java.lang.IllegalArgumentException - private val BASE_INCLUDED_CLASS_NAME_GLOBS = listOf( "**", // everything ) +// We use both a strong indicator for running as a Bazel test together with an indicator for a +// Bazel coverage run to rule out false positives. +private val IS_BAZEL_COVERAGE_RUN = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null && + System.getenv("COVERAGE_DIR") != null + +private val ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE = listOf( + "com.google.testing.coverage.**", + "org.jacoco.**", +) + private val BASE_EXCLUDED_CLASS_NAME_GLOBS = listOf( + // JDK internals "\\[**", // array types - "com.code_intelligence.jazzer.**", - "com.sun.**", // package for Proxy objects "java.**", "javax.**", - "jaz.Ter", // safe companion of the honeypot class used by sanitizers - "jaz.Zer", // honeypot class used by sanitizers "jdk.**", - "kotlin.**", "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 +) + if (IS_BAZEL_COVERAGE_RUN) ADDITIONAL_EXCLUDED_NAME_GLOBS_FOR_BAZEL_COVERAGE else listOf() class ClassNameGlobber(includes: List<String>, excludes: List<String>) { // If no include globs are provided, start with all classes. - private val includeMatchers = (if (includes.isEmpty()) BASE_INCLUDED_CLASS_NAME_GLOBS else includes) + 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. diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt index 31a61740..30f6fb30 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt @@ -14,7 +14,7 @@ @file:JvmName("ExceptionUtils") -package com.code_intelligence.jazzer.runtime +package com.code_intelligence.jazzer.utils import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow import java.lang.management.ManagementFactory @@ -163,4 +163,10 @@ fun dumpAllStackTraces() { } System.err.println() } + System.err.println("Garbage collector stats:") + System.err.println( + ManagementFactory.getGarbageCollectorMXBeans().joinToString("\n", "\n", "\n") { + "${it.name}: ${it.collectionCount} collections took ${it.collectionTime}ms" + } + ) } diff --git a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt index d88c3e18..e7165e55 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.code_intelligence.jazzer.runtime +package com.code_intelligence.jazzer.utils import java.util.jar.Manifest object ManifestUtils { - const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class" + private const val FUZZ_TARGET_CLASS = "Jazzer-Fuzz-Target-Class" const val HOOK_CLASSES = "Jazzer-Hook-Classes" fun combineManifestValues(attribute: String): List<String> { diff --git a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt index af8cce9b..1b399baf 100644 --- a/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt +++ b/agent/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt @@ -17,6 +17,8 @@ package com.code_intelligence.jazzer.utils import java.lang.reflect.Executable import java.lang.reflect.Method +import java.nio.ByteBuffer +import java.nio.channels.FileChannel val Class<*>.descriptor: String get() = when { @@ -80,3 +82,26 @@ fun simpleFastHash(vararg strings: String): Int { } return hash } + +/** + * Reads the [FileChannel] to the end as a UTF-8 string. + */ +fun FileChannel.readFully(): String { + check(size() <= Int.MAX_VALUE) + val buffer = ByteBuffer.allocate(size().toInt()) + while (buffer.hasRemaining()) { + when (read(buffer)) { + 0 -> throw IllegalStateException("No bytes read") + -1 -> break + } + } + return String(buffer.array()) +} + +/** + * Appends [string] to the end of the [FileChannel]. + */ +fun FileChannel.append(string: String) { + position(size()) + write(ByteBuffer.wrap(string.toByteArray())) +} diff --git a/agent/src/main/java/jaz/BUILD.bazel b/agent/src/main/java/jaz/BUILD.bazel new file mode 100644 index 00000000..c6cdcf13 --- /dev/null +++ b/agent/src/main/java/jaz/BUILD.bazel @@ -0,0 +1,8 @@ +filegroup( + name = "jaz", + srcs = [ + "Ter.java", + "Zer.java", + ], + visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/api:__pkg__"], +) diff --git a/sanitizers/src/main/java/jaz/Ter.java b/agent/src/main/java/jaz/Ter.java index 7814396f..7814396f 100644 --- a/sanitizers/src/main/java/jaz/Ter.java +++ b/agent/src/main/java/jaz/Ter.java diff --git a/agent/src/main/java/jaz/Zer.java b/agent/src/main/java/jaz/Zer.java new file mode 100644 index 00000000..08ca3d2e --- /dev/null +++ b/agent/src/main/java/jaz/Zer.java @@ -0,0 +1,234 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaz; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.Jazzer; +import java.io.Closeable; +import java.io.Flushable; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.Function; + +/** + * A honeypot class that reports a finding on initialization. + * + * Class loading based on externally controlled data could lead to RCE + * depending on available classes on the classpath. Even if no applicable + * gadget class is available, allowing input to control class loading is a bad + * idea and should be prevented. A finding is generated whenever the class + * is loaded and initialized, regardless of its further use. + * <p> + * This class needs to implement {@link Serializable} to be considered in + * deserialization scenarios. It also implements common constructors, getter + * and setter and common interfaces to increase chances of passing + * deserialization checks. + * <p> + * <b>Note</b>: Jackson provides a nice list of "nasty classes" at + * <a + * href=https://github.com/FasterXML/jackson-databind/blob/2.14/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java>SubTypeValidator</a>. + * <p> + * <b>Note</b>: This class must not be referenced in any way by the rest of the code, not even + * statically. When referring to it, always use its hardcoded class name {@code jaz.Zer}. + */ +@SuppressWarnings({"rawtypes", "unused"}) +public class Zer + implements Serializable, Cloneable, Comparable<Zer>, Comparator, Closeable, Flushable, Iterable, + Iterator, Runnable, Callable, Function, Collection, List { + static final long serialVersionUID = 42L; + + static { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n" + + "Unrestricted class loading based on externally controlled data may allow\n" + + "remote code execution depending on available classes on the classpath.")); + } + + // Common constructors + + public Zer() {} + + public Zer(String arg1) {} + + public Zer(String arg1, Throwable arg2) {} + + // Getter/Setter + + public Object getJaz() { + return this; + } + + public void setJaz(String jaz) {} + + // Common interface stubs + + @Override + public void close() {} + + @Override + public void flush() {} + + @Override + public int compareTo(Zer o) { + return 0; + } + + @Override + public int compare(Object o1, Object o2) { + return 0; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean contains(Object o) { + return false; + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public boolean add(Object o) { + return false; + } + + @Override + public boolean remove(Object o) { + return false; + } + + @Override + public boolean addAll(Collection c) { + return false; + } + + @Override + public boolean addAll(int index, Collection c) { + return false; + } + + @Override + public void clear() {} + + @Override + public Object get(int index) { + return this; + } + + @Override + public Object set(int index, Object element) { + return this; + } + + @Override + public void add(int index, Object element) {} + + @Override + public Object remove(int index) { + return this; + } + + @Override + public int indexOf(Object o) { + return 0; + } + + @Override + public int lastIndexOf(Object o) { + return 0; + } + + @Override + @SuppressWarnings("ConstantConditions") + public ListIterator listIterator() { + return null; + } + + @Override + @SuppressWarnings("ConstantConditions") + public ListIterator listIterator(int index) { + return null; + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this; + } + + @Override + public boolean retainAll(Collection c) { + return false; + } + + @Override + public boolean removeAll(Collection c) { + return false; + } + + @Override + public boolean containsAll(Collection c) { + return false; + } + + @Override + public Object[] toArray(Object[] a) { + return new Object[0]; + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public void run() {} + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Object next() { + return this; + } + + @Override + public Object call() throws Exception { + return this; + } + + @Override + public Object apply(Object o) { + return this; + } + + @Override + @SuppressWarnings("MethodDoesntCallSuperMethod") + public Object clone() { + return this; + } +} diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel b/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel deleted file mode 100644 index 6b75fb8b..00000000 --- a/agent/src/main/native/com/code_intelligence/jazzer/replay/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") - -cc_jni_library( - name = "replay", - srcs = [ - "com_code_intelligence_jazzer_replay_Replayer.cpp", - ], - visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"], - deps = [ - "//agent/src/main/java/com/code_intelligence/jazzer/replay:replay.hdrs", - "//driver:fuzzed_data_provider", - ], -) diff --git a/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp b/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp deleted file mode 100644 index c4bdfcfb..00000000 --- a/agent/src/main/native/com/code_intelligence/jazzer/replay/com_code_intelligence_jazzer_replay_Replayer.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "com_code_intelligence_jazzer_replay_Replayer.h" - -#include <jni.h> - -#include "driver/fuzzed_data_provider.h" - -namespace { -uint8_t *data = nullptr; -} - -void Java_com_code_1intelligence_jazzer_replay_Replayer_feedFuzzedDataProvider( - JNIEnv *env, jclass, jbyteArray input) { - if (data == nullptr) { - jazzer::SetUpFuzzedDataProvider(*env); - } else { - delete[] data; - } - - std::size_t size = env->GetArrayLength(input); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->FatalError("Failed to get length of input"); - } - data = static_cast<uint8_t *>(operator new(size)); - if (data == nullptr) { - env->FatalError("Failed to allocate memory for a copy of the input"); - } - env->GetByteArrayRegion(input, 0, size, reinterpret_cast<jbyte *>(data)); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->FatalError("Failed to copy input"); - } - jazzer::FeedFuzzedDataProvider(data, size); -} diff --git a/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..7d910474 --- /dev/null +++ b/agent/src/main/native/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,8 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") + +cc_jni_library( + name = "jazzer_signal_handler", + srcs = ["signal_handler.cpp"], + visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/runtime:__pkg__"], + deps = ["//agent/src/main/java/com/code_intelligence/jazzer/runtime:signal_handler.hdrs"], +) diff --git a/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp b/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp new file mode 100644 index 00000000..2600a53a --- /dev/null +++ b/agent/src/main/native/com/code_intelligence/jazzer/runtime/signal_handler.cpp @@ -0,0 +1,40 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <jni.h> + +#include <atomic> +#include <csignal> + +#include "com_code_intelligence_jazzer_runtime_SignalHandler.h" + +#ifdef _WIN32 +// Windows does not have SIGUSR1, which triggers a graceful exit of libFuzzer. +// Instead, trigger a hard exit. +#define SIGUSR1 SIGTERM +#endif + +// Handles SIGINT raised while running Java code. +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_SignalHandler_handleInterrupt( + JNIEnv *, jclass) { + static std::atomic<bool> already_exiting{false}; + if (!already_exiting.exchange(true)) { + // Let libFuzzer exit gracefully when the JVM received SIGINT. + raise(SIGUSR1); + } else { + // Exit libFuzzer forcefully on repeated SIGINTs. + raise(SIGTERM); + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java index 66a85db6..59ef238d 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/AutofuzzTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collections; +import org.junit.BeforeClass; import org.junit.Test; public class AutofuzzTest { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel index 9192ff77..f2537b73 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel +++ b/agent/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel @@ -16,6 +16,7 @@ java_test( ], deps = [ "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", "@maven//:junit_junit", ], ) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java index 0615e9ae..0906d1d5 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/MetaTest.java @@ -22,19 +22,13 @@ import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider; import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.google.json.JsonSanitizer; import java.io.ByteArrayInputStream; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; +import java.util.Map; import org.junit.Test; public class MetaTest { - public static boolean isFive(int arg) { - return arg == 5; - } - - public static boolean intEquals(int arg1, int arg2) { - return arg1 == arg2; - } - public enum TestEnum { FOO, BAR, @@ -42,7 +36,7 @@ public class MetaTest { } @Test - public void testConsume() { + public void testConsume() throws NoSuchMethodException { consumeTestCase(5, "5", Collections.singletonList(5)); consumeTestCase((short) 5, "(short) 5", Collections.singletonList((short) 5)); consumeTestCase(5L, "5L", Collections.singletonList(5L)); @@ -121,6 +115,52 @@ public class MetaTest { consumeTestCase(YourAverageJavaClass.class, "com.code_intelligence.jazzer.autofuzz.YourAverageJavaClass.class", Collections.singletonList((byte) 1)); + + Type stringStringMapType = + MetaTest.class.getDeclaredMethod("returnsStringStringMap").getGenericReturnType(); + Map<String, String> expectedMap = + java.util.stream.Stream + .of(new java.util.AbstractMap.SimpleEntry<>("key0", "value0"), + new java.util.AbstractMap.SimpleEntry<>("key1", "value1"), + new java.util.AbstractMap.SimpleEntry<>("key2", (java.lang.String) null)) + .collect(java.util.HashMap::new, + (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll); + consumeTestCase(stringStringMapType, expectedMap, + "java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<java.lang.String, java.lang.String>>of(new java.util.AbstractMap.SimpleEntry<>(\"key0\", \"value0\"), new java.util.AbstractMap.SimpleEntry<>(\"key1\", \"value1\"), new java.util.AbstractMap.SimpleEntry<>(\"key2\", (java.lang.String) null)).collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)", + Arrays.asList((byte) 1, // do not return null for the map + 32, // remaining bytes + (byte) 1, // do not return null for the string + 31, // remaining bytes + "key0", + (byte) 1, // do not return null for the string + 28, // remaining bytes + "value0", + 28, // remaining bytes + 28, // consumeArrayLength + (byte) 1, // do not return null for the string + 27, // remaining bytes + "key1", + (byte) 1, // do not return null for the string + 23, // remaining bytes + "value1", + (byte) 1, // do not return null for the string + 27, // remaining bytes + "key2", + (byte) 0 // *do* return null for the string + )); + } + + private Map<String, String> returnsStringStringMap() { + throw new IllegalStateException( + "Should not be called, only exists to construct its generic return type"); + } + + public static boolean isFive(int arg) { + return arg == 5; + } + + public static boolean intEquals(int arg1, int arg2) { + return arg1 == arg2; } @Test @@ -129,7 +169,7 @@ public class MetaTest { MetaTest.class.getMethod("isFive", int.class), Collections.singletonList(5)); autofuzzTestCase(false, "com.code_intelligence.jazzer.autofuzz.MetaTest.intEquals(5, 4)", MetaTest.class.getMethod("intEquals", int.class, int.class), Arrays.asList(5, 4)); - autofuzzTestCase("foobar", "\"foo\".concat(\"bar\")", + autofuzzTestCase("foobar", "(\"foo\").concat(\"bar\")", String.class.getMethod("concat", String.class), Arrays.asList((byte) 1, 6, "foo", (byte) 1, 6, "bar")); autofuzzTestCase("jazzer", "new java.lang.String(\"jazzer\")", diff --git a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java index 52f19a74..d556beb3 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/autofuzz/TestHelpers.java @@ -24,6 +24,7 @@ import java.io.ByteArrayInputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.List; class TestHelpers { @@ -57,8 +58,7 @@ class TestHelpers { } static void consumeTestCase( - Class<?> type, Object expectedResult, String expectedResultString, List<Object> cannedData) { - assertTrue(expectedResult == null || type.isAssignableFrom(expectedResult.getClass())); + Type type, Object expectedResult, String expectedResultString, List<Object> cannedData) { AutofuzzCodegenVisitor visitor = new AutofuzzCodegenVisitor(); FuzzedDataProvider data = CannedFuzzedDataProvider.create(cannedData); assertGeneralEquals(expectedResult, Meta.consume(data, type, visitor)); diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt index 53efd200..c5a2e156 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/AfterHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyAfterHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(AfterHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(AfterHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalAfterHooksTargetInstance(): AfterHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel index 472d2b98..036559ec 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel @@ -7,6 +7,7 @@ kt_jvm_library( "DynamicTestContract.java", "PatchTestUtils.kt", ], + visibility = ["//visibility:public"], ) wrapped_kt_jvm_test( @@ -130,6 +131,7 @@ wrapped_kt_jvm_test( size = "small", srcs = [ "ReplaceHooks.java", + "ReplaceHooksInit.java", "ReplaceHooksPatchTest.kt", "ReplaceHooksTarget.java", "ReplaceHooksTargetContract.java", diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt index 31e9733c..4fde7ee1 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/BeforeHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyBeforeHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(BeforeHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(BeforeHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalBeforeHooksTargetInstance(): BeforeHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt index 15c88f4c..f2cf2f08 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTest.kt @@ -14,12 +14,37 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes import java.io.File import kotlin.test.assertEquals +/** + * Amends the instrumentation performed by [strategy] to call the map's public static void method + * updated() after every update to coverage counters. + */ +private fun makeTestable(strategy: EdgeCoverageStrategy): EdgeCoverageStrategy = + object : EdgeCoverageStrategy by strategy { + override fun instrumentControlFlowEdge( + mv: MethodVisitor, + edgeId: Int, + variable: Int, + coverageMapInternalClassName: String + ) { + strategy.instrumentControlFlowEdge(mv, edgeId, variable, coverageMapInternalClassName) + mv.visitMethodInsn(Opcodes.INVOKESTATIC, coverageMapInternalClassName, "updated", "()V", false) + } + } + private fun applyInstrumentation(bytecode: ByteArray): ByteArray { - return EdgeCoverageInstrumentor(0, MockCoverageMap::class.java).instrument(bytecode) + return EdgeCoverageInstrumentor( + makeTestable(ClassInstrumentor.defaultEdgeCoverageStrategy), + MockCoverageMap::class.java, + 0 + ).instrument(bytecode) } private fun getOriginalInstrumentationTargetInstance(): DynamicTestContract { @@ -41,26 +66,34 @@ private fun assertControlFlow(expectedLocations: List<Int>) { assertEquals(expectedLocations, MockCoverageMap.locations.toList()) } +@Suppress("unused") class CoverageInstrumentationTest { private val constructorReturn = 0 - private val ifFirstBranch = 1 - @Suppress("unused") - private val ifSecondBranch = 2 - private val ifEnd = 3 - private val outerForCondition = 4 - private val innerForBodyIfFirstRun = 6 - private val innerForBodyIfSecondRun = 5 - private val innerForIncrementCounter = 7 - private val outerForIncrementCounter = 8 - private val afterFooInvocation = 9 - private val beforeReturn = 10 - private val fooAfterBarInvocation = 11 - private val fooBeforeReturn = 12 - private val barAfterMapPutInvocation = 13 - private val barBeforeReturn = 14 - @Suppress("unused") - private val bazReturn = 15 + + private val mapConstructor = 1 + private val addFor0 = 2 + private val addFor1 = 3 + private val addFor2 = 4 + private val addFor3 = 5 + private val addFor4 = 6 + private val addFoobar = 7 + + private val ifTrueBranch = 8 + private val addBlock1 = 9 + private val ifFalseBranch = 10 + private val ifEnd = 11 + + private val outerForCondition = 12 + private val innerForCondition = 13 + private val innerForBodyIfTrueBranch = 14 + private val innerForBodyIfFalseBranch = 15 + private val innerForBodyPutInvocation = 16 + private val outerForIncrementCounter = 17 + + private val afterFooInvocation = 18 + private val fooAfterBarInvocation = 19 + private val barAfterPutInvocation = 20 @Test fun testOriginal() { @@ -72,31 +105,32 @@ class CoverageInstrumentationTest { MockCoverageMap.clear() assertSelfCheck(getInstrumentedInstrumentationTargetInstance()) - val innerForFirstRunControlFlow = mutableListOf<Int>().apply { + val mapControlFlow = listOf(mapConstructor, addFor0, addFor1, addFor2, addFor3, addFor4, addFoobar) + val ifControlFlow = listOf(ifTrueBranch, addBlock1, ifEnd) + val forFirstRunControlFlow = mutableListOf<Int>().apply { + add(outerForCondition) repeat(5) { - addAll(listOf(innerForBodyIfFirstRun, innerForIncrementCounter)) + addAll(listOf(innerForCondition, innerForBodyIfFalseBranch, innerForBodyPutInvocation)) } + add(outerForIncrementCounter) }.toList() - val innerForSecondRunControlFlow = mutableListOf<Int>().apply { + val forSecondRunControlFlow = mutableListOf<Int>().apply { + add(outerForCondition) repeat(5) { - addAll(listOf(innerForBodyIfSecondRun, innerForIncrementCounter)) + addAll(listOf(innerForCondition, innerForBodyIfTrueBranch, innerForBodyPutInvocation)) } + add(outerForIncrementCounter) }.toList() - val outerForControlFlow = - listOf(outerForCondition) + - innerForFirstRunControlFlow + - listOf(outerForIncrementCounter, outerForCondition) + - innerForSecondRunControlFlow + - listOf(outerForIncrementCounter) - + val forControlFlow = forFirstRunControlFlow + forSecondRunControlFlow + val fooCallControlFlow = listOf( + barAfterPutInvocation, fooAfterBarInvocation, afterFooInvocation + ) assertControlFlow( - listOf(constructorReturn, ifFirstBranch, ifEnd) + - outerForControlFlow + - listOf( - barAfterMapPutInvocation, barBeforeReturn, - fooAfterBarInvocation, fooBeforeReturn, - afterFooInvocation, beforeReturn - ) + listOf(constructorReturn) + + mapControlFlow + + ifControlFlow + + forControlFlow + + fooCallControlFlow ) } @@ -109,17 +143,17 @@ class CoverageInstrumentationTest { // The constructor of the target is run only once. val takenOnceEdge = constructorReturn // Control flows through the first if branch once per run. - val takenOnEveryRunEdge = ifFirstBranch + val takenOnEveryRunEdge = ifTrueBranch var lastCounter = 0.toUByte() for (i in 1..600) { assertSelfCheck(target) - assertEquals(1, MockCoverageMap.mem[takenOnceEdge]) + 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() lastCounter = expectedCounter - val actualCounter = MockCoverageMap.mem[takenOnEveryRunEdge].toUByte() + val actualCounter = MockCoverageMap.counters[takenOnEveryRunEdge].toUByte() assertEquals(expectedCounter, actualCounter, "After $i runs:") } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt index 7e7c31c9..ac263dc5 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/HookValidationTest.kt @@ -22,7 +22,8 @@ import kotlin.test.assertFailsWith class HookValidationTest { @Test fun testValidHooks() { - assertEquals(6, loadHooks(ValidHookMocks::class.java).size) + val hooks = Hooks.loadHooks(setOf(ValidHookMocks::class.java.name)).first().hooks + assertEquals(5, hooks.size) } @Test @@ -30,7 +31,8 @@ class HookValidationTest { for (method in InvalidHookMocks::class.java.methods) { if (method.isAnnotationPresent(MethodHook::class.java)) { assertFailsWith<IllegalArgumentException>("Expected ${method.name} to be an invalid hook") { - Hook.verifyAndGetHook(method, method.declaredAnnotations.first() as MethodHook) + val methodHook = method.declaredAnnotations.first() as MethodHook + Hook.createAndVerifyHook(method, methodHook, methodHook.targetClassName) } } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java index 2723ad6e..0df349ca 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/InvalidHookMocks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; +@SuppressWarnings({"unused", "RedundantThrows"}) class InvalidHookMocks { @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.String", targetMethod = "equals") public static void incorrectHookIdType( @@ -45,7 +46,14 @@ class InvalidHookMocks { return true; } - @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.System", targetMethod = "gc", + targetMethodDescriptor = "()V") + public static Object + invalidReplaceVoidMethod(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return null; + } + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.StringBuilder", targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") public static Object invalidReturnType(MethodHandle method, Object thisObject, Object[] arguments, int hookId) @@ -58,4 +66,22 @@ class InvalidHookMocks { public static void primitiveReturnValueMustBeWrapped(MethodHandle method, String thisObject, Object[] arguments, int hookId, boolean returnValue) {} + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") + public static void + replaceOnInitWithoutReturnType( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) throws Throwable {} + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.StringBuilder", + targetMethod = "<init>", targetMethodDescriptor = "(Ljava/lang/String;)V") + public static Object + replaceOnInitWithIncompatibleType( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) throws Throwable { + return new Object(); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") + public static void primitiveReturnType(MethodHandle method, String thisObject, Object[] arguments, + int hookId, boolean returnValue) {} } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java index 787ea493..3ea33d19 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/MockCoverageMap.java @@ -20,8 +20,7 @@ import java.util.Arrays; public class MockCoverageMap { public static final int SIZE = 65536; - public static final ByteBuffer mem = ByteBuffer.allocate(SIZE); - public static int prev_location = 0; // is used in byte code directly + public static final ByteBuffer counters = ByteBuffer.allocate(SIZE); private static final ByteBuffer previous_mem = ByteBuffer.allocate(SIZE); public static ArrayList<Integer> locations = new ArrayList<>(); @@ -29,16 +28,25 @@ public class MockCoverageMap { public static void updated() { int updated_pos = -1; for (int i = 0; i < SIZE; i++) { - if (previous_mem.get(i) != mem.get(i)) { + if (previous_mem.get(i) != counters.get(i)) { updated_pos = i; } } locations.add(updated_pos); - System.arraycopy(mem.array(), 0, previous_mem.array(), 0, SIZE); + System.arraycopy(counters.array(), 0, previous_mem.array(), 0, SIZE); + } + + public static void enlargeIfNeeded(int nextId) { + // This mock coverage map is statically sized. + } + + public static void recordCoverage(int id) { + byte counter = counters.get(id); + counters.put(id, (byte) (counter == -1 ? 1 : counter + 1)); } public static void clear() { - Arrays.fill(mem.array(), (byte) 0); + Arrays.fill(counters.array(), (byte) 0); Arrays.fill(previous_mem.array(), (byte) 0); locations.clear(); } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt index f286d03f..00279c35 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/PatchTestUtils.kt @@ -14,30 +14,40 @@ package com.code_intelligence.jazzer.instrumentor -fun classToBytecode(targetClass: Class<*>): ByteArray { - return ClassLoader - .getSystemClassLoader() - .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! - .use { - it.readBytes() - } -} +import java.io.FileOutputStream -fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { - return BytecodeClassLoader(name, bytecode).loadClass(name) -} +object PatchTestUtils { + @JvmStatic + fun classToBytecode(targetClass: Class<*>): ByteArray { + return ClassLoader + .getSystemClassLoader() + .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!! + .use { + it.readBytes() + } + } -/** - * 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) { - override fun loadClass(name: String): Class<*> { - if (name != className) - return super.loadClass(name) + @JvmStatic + fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> { + return BytecodeClassLoader(name, bytecode).loadClass(name) + } + + @JvmStatic + public fun dumpBytecode(outDir: String, name: String, originalBytecode: ByteArray) { + FileOutputStream("$outDir/$name.class").use { fos -> fos.write(originalBytecode) } + } - return defineClass(className, classBytecode, 0, classBytecode.size) + /** + * 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) { + override fun loadClass(name: String): Class<*> { + if (name != className) + return super.loadClass(name) + return defineClass(className, classBytecode, 0, classBytecode.size) + } } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java index a71e1180..7e31b77b 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooks.java @@ -18,6 +18,7 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.MethodHook; import java.lang.invoke.MethodHandle; +@SuppressWarnings("unused") public class ReplaceHooks { @MethodHook(type = HookType.REPLACE, targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksTarget", @@ -106,4 +107,30 @@ public class ReplaceHooks { patchAbstractListGet(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { return true; } + + @MethodHook(type = HookType.REPLACE, targetClassName = "java.util.Set", targetMethod = "contains", + targetMethodDescriptor = "(Ljava/lang/Object;)Z") + public static boolean + patchSetGet(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return true; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksInit", + targetMethod = "<init>", targetMethodDescriptor = "()V") + public static ReplaceHooksInit + patchInit(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + // Test with subclass + return new ReplaceHooksInit() { + { initialized = true; } + }; + } + + @MethodHook(type = HookType.REPLACE, + targetClassName = "com.code_intelligence.jazzer.instrumentor.ReplaceHooksInit", + targetMethod = "<init>", targetMethodDescriptor = "(ZLjava/lang/String;)V") + public static ReplaceHooksInit + patchInitWithParams(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + return new ReplaceHooksInit(true, ""); + } } diff --git a/driver/testdata/test/SimpleFuzzTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java index 5657e416..da77be81 100644 --- a/driver/testdata/test/SimpleFuzzTarget.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package test; +package com.code_intelligence.jazzer.instrumentor; -class SimpleFuzzTarget { - public static void fuzzerTestOneInput(byte[] input) { - String inputString = new String(input); - System.err.println("got input " + inputString); - if (inputString.startsWith("crash")) { - throw new RuntimeException("exception triggered in fuzz target"); - } +public class ReplaceHooksInit { + public boolean initialized; + + public ReplaceHooksInit() {} + + @SuppressWarnings("unused") + public ReplaceHooksInit(boolean initialized, String ignored) { + this.initialized = initialized; } } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt index 76fb53e5..b6266d12 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksPatchTest.kt @@ -14,11 +14,14 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File private fun applyReplaceHooks(bytecode: ByteArray): ByteArray { - return HookInstrumentor(loadHooks(ReplaceHooks::class.java), false).instrument(bytecode) + val hooks = Hooks.loadHooks(setOf(ReplaceHooks::class.java.name)).first().hooks + return HookInstrumentor(hooks, false).instrument(bytecode) } private fun getOriginalReplaceHooksTargetInstance(): ReplaceHooksTargetContract { diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java index 7a4b89f8..fadbdf80 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksTarget.java @@ -15,9 +15,9 @@ package com.code_intelligence.jazzer.instrumentor; import java.security.SecureRandom; -import java.util.AbstractList; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; // selfCheck() only passes with the hooks in ReplaceHooks.java applied. @@ -56,10 +56,16 @@ public class ReplaceHooksTarget implements ReplaceHooksTargetContract { shouldCallPass(); } - AbstractList<Boolean> boolList = new ArrayList<>(); + ArrayList<Boolean> boolList = new ArrayList<>(); boolList.add(false); results.put("arrayListGet", boolList.get(0)); + HashSet<Boolean> boolSet = new HashSet<>(); + results.put("stringSetGet", boolSet.contains(Boolean.TRUE)); + + results.put("shouldInitialize", new ReplaceHooksInit().initialized); + results.put("shouldInitializeWithParams", new ReplaceHooksInit(false, "foo").initialized); + return results; } diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java index 48f16e60..d8e28881 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTarget.java @@ -37,6 +37,7 @@ public class TraceDataFlowInstrumentationTarget implements DynamicTestContract { volatile int switchValue = 1200; + @SuppressWarnings("ReturnValueIgnored") @Override public Map<String, Boolean> selfCheck() { Map<String, Boolean> results = new HashMap<>(); diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt index c6fd218f..4d4b0318 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/TraceDataFlowInstrumentationTest.kt @@ -14,6 +14,8 @@ package com.code_intelligence.jazzer.instrumentor +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.bytecodeToClass +import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode import org.junit.Test import java.io.File diff --git a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java index 06bed141..a919242b 100644 --- a/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java +++ b/agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ValidHookMocks.java @@ -27,10 +27,6 @@ class ValidHookMocks { public static void validAfterHook(MethodHandle method, String thisObject, Object[] arguments, int hookId, Boolean returnValue) {} - @MethodHook(type = HookType.AFTER, targetClassName = "java.lang.String", targetMethod = "equals") - public static void validAfterHook2(MethodHandle method, String thisObject, Object[] arguments, - int hookId, boolean returnValue) {} - @MethodHook(type = HookType.REPLACE, targetClassName = "java.lang.String", targetMethod = "equals", targetMethodDescriptor = "(Ljava/lang/Object;)Z") public static Boolean diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel new file mode 100644 index 00000000..97ac4f62 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel @@ -0,0 +1,38 @@ +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") + +java_test( + name = "FuzzedDataProviderImplTest", + srcs = ["FuzzedDataProviderImplTest.java"], + use_testrunner = False, + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + ], +) + +java_test( + name = "RecordingFuzzedDataProviderTest", + srcs = [ + "RecordingFuzzedDataProviderTest.java", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", + "@maven//:junit_junit", + ], +) + +java_test( + name = "TraceCmpHooksTest", + srcs = [ + "TraceCmpHooksTest.java", + ], + target_compatible_with = SKIP_ON_WINDOWS, + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + "@maven//:junit_junit", + ], +) diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java new file mode 100644 index 00000000..5e922fc0 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/FuzzedDataProviderImplTest.java @@ -0,0 +1,225 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class FuzzedDataProviderImplTest { + public static void main(String[] args) { + try (FuzzedDataProviderImpl fuzzedDataProvider = + FuzzedDataProviderImpl.withJavaData(INPUT_BYTES)) { + verifyFuzzedDataProvider(fuzzedDataProvider); + } + } + + private strictfp static void verifyFuzzedDataProvider(FuzzedDataProvider data) { + assertEqual(true, data.consumeBoolean()); + + assertEqual((byte) 0x7F, data.consumeByte()); + assertEqual((byte) 0x14, data.consumeByte((byte) 0x12, (byte) 0x22)); + + assertEqual(0x12345678, data.consumeInt()); + assertEqual(-0x12345600, data.consumeInt(-0x12345678, -0x12345600)); + assertEqual(0x12345679, data.consumeInt(0x12345678, 0x12345679)); + + assertEqual(true, Arrays.equals(new byte[] {0x01, 0x02}, data.consumeBytes(2))); + + assertEqual("jazzer", data.consumeString(6)); + assertEqual("ja\u0000zer", data.consumeString(6)); + assertEqual("€ß", data.consumeString(2)); + + assertEqual("jazzer", data.consumeAsciiString(6)); + assertEqual("ja\u0000zer", data.consumeAsciiString(6)); + assertEqual("\u0062\u0002\u002C\u0043\u001F", data.consumeAsciiString(5)); + + assertEqual(true, + Arrays.equals(new boolean[] {false, false, true, false, true}, data.consumeBooleans(5))); + assertEqual(true, + Arrays.equals(new long[] {0x0123456789abdcefL, 0xfedcba9876543210L}, data.consumeLongs(2))); + + assertEqual((float) 0.28969181, data.consumeProbabilityFloat()); + assertEqual(0.086814121166605432, data.consumeProbabilityDouble()); + assertEqual((float) 0.30104411, data.consumeProbabilityFloat()); + assertEqual(0.96218831486039413, data.consumeProbabilityDouble()); + + assertEqual((float) -2.8546307e+38, data.consumeRegularFloat()); + assertEqual(8.0940194040236032e+307, data.consumeRegularDouble()); + assertEqual((float) 271.49084, data.consumeRegularFloat((float) 123.0, (float) 777.0)); + assertEqual(30.859126145478349, data.consumeRegularDouble(13.37, 31.337)); + + assertEqual((float) 0.0, data.consumeFloat()); + assertEqual((float) -0.0, data.consumeFloat()); + assertEqual(Float.POSITIVE_INFINITY, data.consumeFloat()); + assertEqual(Float.NEGATIVE_INFINITY, data.consumeFloat()); + assertEqual(true, Float.isNaN(data.consumeFloat())); + assertEqual(Float.MIN_VALUE, data.consumeFloat()); + assertEqual(-Float.MIN_VALUE, data.consumeFloat()); + assertEqual(Float.MIN_NORMAL, data.consumeFloat()); + assertEqual(-Float.MIN_NORMAL, data.consumeFloat()); + assertEqual(Float.MAX_VALUE, data.consumeFloat()); + assertEqual(-Float.MAX_VALUE, data.consumeFloat()); + + assertEqual(0.0, data.consumeDouble()); + assertEqual(-0.0, data.consumeDouble()); + assertEqual(Double.POSITIVE_INFINITY, data.consumeDouble()); + assertEqual(Double.NEGATIVE_INFINITY, data.consumeDouble()); + assertEqual(true, Double.isNaN(data.consumeDouble())); + assertEqual(Double.MIN_VALUE, data.consumeDouble()); + assertEqual(-Double.MIN_VALUE, data.consumeDouble()); + assertEqual(Double.MIN_NORMAL, data.consumeDouble()); + assertEqual(-Double.MIN_NORMAL, data.consumeDouble()); + assertEqual(Double.MAX_VALUE, data.consumeDouble()); + assertEqual(-Double.MAX_VALUE, data.consumeDouble()); + + int[] array = {0, 1, 2, 3, 4}; + assertEqual(4, data.pickValue(array)); + assertEqual(2, (int) data.pickValue(Arrays.stream(array).boxed().toArray())); + assertEqual(3, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toList()))); + assertEqual(2, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toSet()))); + + // Buffer is almost depleted at this point. + assertEqual(7, data.remainingBytes()); + assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(3))); + assertEqual(7, data.remainingBytes()); + assertEqual(true, Arrays.equals(new int[] {0x12345678}, data.consumeInts(3))); + assertEqual(3, data.remainingBytes()); + assertEqual(0x123456L, data.consumeLong()); + + // Buffer has been fully consumed at this point + assertEqual(0, data.remainingBytes()); + assertEqual(0, data.consumeInt()); + assertEqual(0.0, data.consumeDouble()); + assertEqual(-13.37, data.consumeRegularDouble(-13.37, 31.337)); + assertEqual(true, Arrays.equals(new byte[0], data.consumeBytes(4))); + assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(4))); + assertEqual("", data.consumeRemainingAsAsciiString()); + assertEqual("", data.consumeRemainingAsString()); + assertEqual("", data.consumeAsciiString(100)); + assertEqual("", data.consumeString(100)); + } + + private static <T extends Comparable<T>> void assertEqual(T a, T b) { + if (a.compareTo(b) != 0) { + throw new IllegalArgumentException("Expected: " + a + ", got: " + b); + } + } + + private static final byte[] INPUT_BYTES = new byte[] { + // Bytes read from the start + 0x01, 0x02, // consumeBytes(2): {0x01, 0x02} + + 'j', 'a', 'z', 'z', 'e', 'r', // consumeString(6): "jazzer" + 'j', 'a', 0x00, 'z', 'e', 'r', // consumeString(6): "ja\u0000zer" + (byte) 0xE2, (byte) 0x82, (byte) 0xAC, (byte) 0xC3, (byte) 0x9F, // consumeString(2): "€ẞ" + + 'j', 'a', 'z', 'z', 'e', 'r', // consumeAsciiString(6): "jazzer" + 'j', 'a', 0x00, 'z', 'e', 'r', // consumeAsciiString(6): "ja\u0000zer" + (byte) 0xE2, (byte) 0x82, (byte) 0xAC, (byte) 0xC3, + (byte) 0x9F, // consumeAsciiString(5): "\u0062\u0002\u002C\u0043\u001F" + + 0, 0, 1, 0, 1, // consumeBooleans(5): { false, false, true, false, true } + (byte) 0xEF, (byte) 0xDC, (byte) 0xAB, (byte) 0x89, 0x67, 0x45, 0x23, 0x01, 0x10, 0x32, 0x54, + 0x76, (byte) 0x98, (byte) 0xBA, (byte) 0xDC, (byte) 0xFE, + // consumeLongs(2): { 0x0123456789ABCDEF, 0xFEDCBA9876543210 } + + 0x78, 0x56, 0x34, 0x12, // consumeInts(3): { 0x12345678 } + 0x56, 0x34, 0x12, // consumeLong(): + + // Bytes read from the end + 0x02, 0x03, 0x02, 0x04, // 4x pickValue in array with five elements + + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 10, // -max for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 9, // max for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 8, // -min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 7, // min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 6, // -denorm_min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 5, // denorm_min for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 4, // NaN for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 3, // -infinity for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 2, // infinity for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 1, // -0.0 for next consumeDouble + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12, 0x34, 0x56, + 0x78, // consumed but unused by consumeDouble() + 0, // 0.0 for next consumeDouble + + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 10, // -max for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 9, // max for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 8, // -min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 7, // min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 6, // -denorm_min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 5, // denorm_min for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 4, // NaN for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 3, // -infinity for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 2, // infinity for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 1, // -0.0 for next consumeFloat + 0x12, 0x34, 0x56, 0x78, (byte) 0x90, // consumed but unused by consumeFloat() + 0, // 0.0 for next consumeFloat + + (byte) 0x88, (byte) 0xAB, 0x61, (byte) 0xCB, 0x32, (byte) 0xEB, 0x30, (byte) 0xF9, + // consumeDouble(13.37, 31.337): 30.859126145478349 (small range) + 0x51, (byte) 0xF6, 0x1F, 0x3A, // consumeFloat(123.0, 777.0): 271.49084 (small range) + 0x11, 0x4D, (byte) 0xFD, 0x54, (byte) 0xD6, 0x3D, 0x43, 0x73, 0x39, + // consumeRegularDouble(): 8.0940194040236032e+307 + 0x16, (byte) 0xCF, 0x3D, 0x29, 0x4A, // consumeRegularFloat(): -2.8546307e+38 + + 0x61, (byte) 0xCB, 0x32, (byte) 0xEB, 0x30, (byte) 0xF9, 0x51, (byte) 0xF6, + // consumeProbabilityDouble(): 0.96218831486039413 + 0x1F, 0x3A, 0x11, 0x4D, // consumeProbabilityFloat(): 0.30104411 + (byte) 0xFD, 0x54, (byte) 0xD6, 0x3D, 0x43, 0x73, 0x39, 0x16, + // consumeProbabilityDouble(): 0.086814121166605432 + (byte) 0xCF, 0x3D, 0x29, 0x4A, // consumeProbabilityFloat(): 0.28969181 + + 0x01, // consumeInt(0x12345678, 0x12345679): 0x12345679 + 0x78, // consumeInt(-0x12345678, -0x12345600): -0x12345600 + 0x78, 0x56, 0x34, 0x12, // consumeInt(): 0x12345678 + + 0x02, // consumeByte(0x12, 0x22): 0x14 + 0x7F, // consumeByte(): 0x7F + + 0x01, // consumeBool(): true + }; +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java new file mode 100644 index 00000000..d58a5ca9 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProviderTest.java @@ -0,0 +1,214 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.runtime; + +import com.code_intelligence.jazzer.api.CannedFuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.junit.Assert; +import org.junit.Test; + +public class RecordingFuzzedDataProviderTest { + @Test + public void testRecordingFuzzedDataProvider() throws IOException { + FuzzedDataProvider mockData = new MockFuzzedDataProvider(); + String referenceResult = sampleFuzzTarget(mockData); + + FuzzedDataProvider recordingMockData = + RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(mockData); + Assert.assertEquals(referenceResult, sampleFuzzTarget(recordingMockData)); + + String cannedMockDataString = + RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy(recordingMockData); + FuzzedDataProvider cannedMockData = new CannedFuzzedDataProvider(cannedMockDataString); + Assert.assertEquals(referenceResult, sampleFuzzTarget(cannedMockData)); + } + + private String sampleFuzzTarget(FuzzedDataProvider data) { + StringBuilder result = new StringBuilder(); + result.append(data.consumeString(10)); + int[] ints = data.consumeInts(5); + result.append(Arrays.stream(ints).mapToObj(Integer::toString).collect(Collectors.joining(","))); + result.append(data.pickValue(ints)); + result.append(data.consumeString(20)); + result.append(data.pickValues(Arrays.stream(ints).boxed().collect(Collectors.toSet()), 5) + .stream() + .map(Integer::toHexString) + .collect(Collectors.joining(","))); + result.append(data.remainingBytes()); + return result.toString(); + } + + private static final class MockFuzzedDataProvider implements FuzzedDataProvider { + @Override + public boolean consumeBoolean() { + return true; + } + + @Override + public boolean[] consumeBooleans(int maxLength) { + return new boolean[] {false, true}; + } + + @Override + public byte consumeByte() { + return 2; + } + + @Override + public byte consumeByte(byte min, byte max) { + return max; + } + + @Override + public short consumeShort() { + return 2; + } + + @Override + public short consumeShort(short min, short max) { + return min; + } + + @Override + public short[] consumeShorts(int maxLength) { + return new short[] {2, 4, 7}; + } + + @Override + public int consumeInt() { + return 5; + } + + @Override + public int consumeInt(int min, int max) { + return max; + } + + @Override + public int[] consumeInts(int maxLength) { + return IntStream.range(0, maxLength).toArray(); + } + + @Override + public long consumeLong() { + return 42; + } + + @Override + public long consumeLong(long min, long max) { + return min; + } + + @Override + public long[] consumeLongs(int maxLength) { + return LongStream.range(0, maxLength).toArray(); + } + + @Override + public float consumeFloat() { + return Float.NaN; + } + + @Override + public float consumeRegularFloat() { + return 0.3f; + } + + @Override + public float consumeRegularFloat(float min, float max) { + return min; + } + + @Override + public float consumeProbabilityFloat() { + return 0.2f; + } + + @Override + public double consumeDouble() { + return Double.NaN; + } + + @Override + public double consumeRegularDouble(double min, double max) { + return max; + } + + @Override + public double consumeRegularDouble() { + return Math.PI; + } + + @Override + public double consumeProbabilityDouble() { + return 0.5; + } + + @Override + public char consumeChar() { + return 'C'; + } + + @Override + public char consumeChar(char min, char max) { + return min; + } + + @Override + public char consumeCharNoSurrogates() { + return 'C'; + } + + @Override + public String consumeAsciiString(int maxLength) { + return "foobar"; + } + + @Override + public String consumeString(int maxLength) { + return "foo€ä"; + } + + @Override + public String consumeRemainingAsAsciiString() { + return "foobar"; + } + + @Override + public String consumeRemainingAsString() { + return "foobar"; + } + + @Override + public byte[] consumeBytes(int maxLength) { + return new byte[maxLength]; + } + + @Override + public byte[] consumeRemainingAsBytes() { + return new byte[] {1}; + } + + @Override + public int remainingBytes() { + return 1; + } + } +} diff --git a/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java b/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java new file mode 100644 index 00000000..9275ca30 --- /dev/null +++ b/agent/src/test/java/com/code_intelligence/jazzer/runtime/TraceCmpHooksTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.runtime; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TraceCmpHooksTest { + private static final ExecutorService ES = Executors.newFixedThreadPool(5); + + @Test + public void cmpHookShouldHandleConcurrentModifications() throws InterruptedException { + String arg = "test"; + Map<String, Object> map = new HashMap<>(); + map.put(arg, arg); + + // Add elements to map asynchronously + Function<Integer, Runnable> put = (final Integer num) -> () -> { + map.put(String.valueOf(num), num); + }; + for (int i = 0; i < 1_000_000; i++) { + ES.submit(put.apply(i)); + } + + // Call hook + for (int i = 0; i < 1_000; i++) { + TraceCmpHooks.mapGet(null, map, new Object[] {arg}, 1, null); + } + + ES.shutdown(); + // noinspection ResultOfMethodCallIgnored + ES.awaitTermination(5, TimeUnit.SECONDS); + } +} diff --git a/agent/verify_shading.sh b/agent/verify_shading.sh new file mode 100755 index 00000000..5742476c --- /dev/null +++ b/agent/verify_shading.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env sh +# Copyright 2022 Code Intelligence GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# List all files in the jar and exclude an allowed list of files. +# Since grep fails if there is no match, ! ... | grep ... fails if there is a +# match. +! external/local_jdk/bin/jar tf "$1" | \ + grep -v \ + -e '^build-data.properties$' \ + -e '^com/$' \ + -e '^com/code_intelligence/$' \ + -e '^com/code_intelligence/jazzer/' \ + -e '^jaz/' \ + -e '^win32-x86/' \ + -e '^win32-x86-64/' \ + -e '^META-INF/' diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel index 1e2348c1..e69de29b 100644 --- a/bazel/BUILD.bazel +++ b/bazel/BUILD.bazel @@ -1,6 +0,0 @@ -java_library( - name = "fuzz_target_test_wrapper", - srcs = ["FuzzTargetTestWrapper.java"], - visibility = ["//:__subpackages__"], - deps = ["@bazel_tools//tools/java/runfiles"], -) diff --git a/bazel/FuzzTargetTestWrapper.java b/bazel/FuzzTargetTestWrapper.java deleted file mode 100644 index 59b15844..00000000 --- a/bazel/FuzzTargetTestWrapper.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import com.google.devtools.build.runfiles.Runfiles; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class FuzzTargetTestWrapper { - public static void main(String[] args) { - String driverActualPath; - String jarActualPath; - Runfiles runfiles; - try { - runfiles = Runfiles.create(); - driverActualPath = runfiles.rlocation(rlocationPath(args[0])); - jarActualPath = runfiles.rlocation(rlocationPath(args[1])); - } catch (IOException | ArrayIndexOutOfBoundsException e) { - e.printStackTrace(); - System.exit(1); - return; - } - - ProcessBuilder processBuilder = new ProcessBuilder(); - Map<String, String> environment = processBuilder.environment(); - // Ensure that Jazzer can find its runfiles. - environment.putAll(runfiles.getEnvVars()); - - // Crashes will be available as test outputs. These are cleared on the next run, - // so this is only useful for examples. - String outputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); - List<String> command = - Stream - .concat(Stream.of(driverActualPath, String.format("-artifact_prefix=%s/", outputDir), - String.format("--reproducer_path=%s", outputDir), "-seed=2735196724", - String.format("--cp=%s", jarActualPath)), - Arrays.stream(args).skip(2)) - .collect(Collectors.toList()); - processBuilder.inheritIO(); - processBuilder.command(command); - - try { - int exitCode = processBuilder.start().waitFor(); - // Assert that we either found a crash in Java (exit code 77) or a sanitizer crash (exit code - // 76). - if (exitCode != 76 && exitCode != 77) { - System.exit(3); - } - String[] outputFiles = new File(outputDir).list(); - if (outputFiles == null) { - System.exit(4); - } - // Verify that libFuzzer dumped a crashing input. - if (Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("crash-"))) { - System.exit(5); - } - } catch (IOException | InterruptedException e) { - e.printStackTrace(); - System.exit(2); - } - System.exit(0); - } - - // Turns the result of Bazel's `$(rootpath ...)` into the correct format for rlocation. - private static String rlocationPath(String rootpath) { - if (rootpath.startsWith("external/")) { - return rootpath.substring("external/".length()); - } else { - return "jazzer/" + rootpath; - } - } -} diff --git a/bazel/cc.bzl b/bazel/cc.bzl deleted file mode 100644 index 65d298d5..00000000 --- a/bazel/cc.bzl +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2021 Code Intelligence GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -def _add_cxxopt_std_17_impl(settings, attr): - STD_CXX_17_CXXOPTS = ["/std:c++17" if attr.is_windows else "-std=c++17"] - return { - "//command_line_option:cxxopt": settings["//command_line_option:cxxopt"] + STD_CXX_17_CXXOPTS, - } - -_add_cxxopt_std_17 = transition( - implementation = _add_cxxopt_std_17_impl, - inputs = [ - "//command_line_option:cxxopt", - ], - outputs = [ - "//command_line_option:cxxopt", - ], -) - -def _cc_17_library_impl(ctx): - library = ctx.attr.library[0] - return [ - # Workaround for https://github.com/bazelbuild/bazel/issues/9442. - DefaultInfo( - data_runfiles = library[DefaultInfo].data_runfiles, - default_runfiles = library[DefaultInfo].default_runfiles, - files = library[DefaultInfo].files, - ), - library[CcInfo], - ] - -_cc_17_library = rule( - implementation = _cc_17_library_impl, - attrs = { - "is_windows": attr.bool(), - "library": attr.label( - cfg = _add_cxxopt_std_17, - mandatory = True, - providers = [CcInfo], - ), - "_allowlist_function_transition": attr.label( - default = "@bazel_tools//tools/allowlists/function_transition_allowlist", - ), - }, - provides = [CcInfo], -) - -# A cc_library that is built with -std=c++17, including all its transitive -# dependencies. This is redundant while developing Jazzer itself as the .bazelrc -# sets this flag for all build commands, but is needed when Jazzer is included -# as an external workspace. -def cc_17_library(name, visibility = None, **kwargs): - library_name = name + "_original_do_not_use_" - kwargs.setdefault("tags", []).append("manual") - native.cc_library( - name = library_name, - visibility = ["//visibility:private"], - **kwargs - ) - - _cc_17_library( - name = name, - is_windows = select({ - "@platforms//os:windows": True, - "//conditions:default": False, - }), - library = library_name, - visibility = visibility, - ) diff --git a/bazel/coverage/BUILD.bazel b/bazel/coverage/BUILD.bazel new file mode 100644 index 00000000..b3dc9861 --- /dev/null +++ b/bazel/coverage/BUILD.bazel @@ -0,0 +1,10 @@ +# Run this target to generate and open an HTML coverage report. +# Takes the same arguments as `bazel coverage`, but after a double dash (`--`). +# The default is to run `bazel coverage //...`, which accumulates the coverage of all tests. +sh_binary( + name = "coverage", + srcs = ["coverage.sh"], + data = [ + "@genhtml//file:genhtml", + ], +) diff --git a/bazel/coverage/coverage.sh b/bazel/coverage/coverage.sh new file mode 100755 index 00000000..626fdc70 --- /dev/null +++ b/bazel/coverage/coverage.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh +# Copyright 2022 Code Intelligence GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Use just like `bazel test` to generate and open an HTML coverage report. +# Requires a local installation of Perl. + +RUNFILES_ROOT=$PWD +cd "$BUILD_WORKSPACE_DIRECTORY" || exit 1 +if ! bazel coverage "${@:-//...}"; +then + exit $? +fi +"$RUNFILES_ROOT"/../genhtml/file/genhtml -o coverage \ + --prefix "$PWD" \ + --title "bazel coverage ${*:-//...}" \ + bazel-out/_coverage/_coverage_report.dat +xdg-open coverage/index.html > /dev/null 2>&1 diff --git a/bazel/fuzz_target.bzl b/bazel/fuzz_target.bzl index bd90e500..c70543bd 100644 --- a/bazel/fuzz_target.bzl +++ b/bazel/fuzz_target.bzl @@ -17,7 +17,7 @@ def java_fuzz_target_test( target_class = None, deps = [], hook_classes = [], - native_libs = [], + data = [], sanitizer = None, visibility = None, tags = [], @@ -25,6 +25,12 @@ def java_fuzz_target_test( srcs = [], size = None, timeout = None, + env = None, + verify_crash_input = True, + verify_crash_reproducer = True, + expect_crash = True, + # Default is that the reproducer does not throw any exception. + expected_findings = [], **kwargs): target_name = name + "_target" deploy_manifest_lines = [] @@ -47,8 +53,6 @@ def java_fuzz_target_test( **kwargs ) - additional_args = [] - if sanitizer == None: driver = "//driver:jazzer_driver" elif sanitizer == "address": @@ -60,19 +64,39 @@ def java_fuzz_target_test( native.java_test( name = name, - runtime_deps = ["//bazel:fuzz_target_test_wrapper"], + runtime_deps = [ + "//bazel/tools/java:fuzz_target_test_wrapper", + "//agent:jazzer_api_deploy.jar", + ":%s_deploy.jar" % target_name, + ], + jvm_flags = [ + # Use the same memory settings for reproducers as those suggested by Jazzer when + # encountering an OutOfMemoryError. + "-Xmx1620m", + # Ensure that reproducers can be compiled even if they contain UTF-8 characters. + "-Dfile.encoding=UTF-8", + ], size = size or "enormous", timeout = timeout or "moderate", args = [ "$(rootpath %s)" % driver, + "$(rootpath //agent:jazzer_api_deploy.jar)", "$(rootpath :%s_deploy.jar)" % target_name, - ] + additional_args + fuzzer_args, + str(verify_crash_input), + str(verify_crash_reproducer), + str(expect_crash), + # args are shell tokenized and thus quotes are required in the case where + # expected_findings is empty. + "'" + ",".join(expected_findings) + "'", + ] + fuzzer_args, data = [ ":%s_deploy.jar" % target_name, - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", + "//agent:jazzer_api_deploy.jar", driver, - ] + native_libs, - main_class = "FuzzTargetTestWrapper", + ] + data, + env = env, + main_class = "com.code_intelligence.jazzer.tools.FuzzTargetTestWrapper", use_testrunner = False, tags = tags, visibility = visibility, diff --git a/bazel/jar.bzl b/bazel/jar.bzl new file mode 100644 index 00000000..b4de3623 --- /dev/null +++ b/bazel/jar.bzl @@ -0,0 +1,58 @@ +# Copyright 2022 Code Intelligence GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def _strip_jar(ctx): + out_jar = ctx.outputs.out + if out_jar == None: + out_jar = ctx.actions.declare_file(ctx.attr.name + ".jar") + + args = ctx.actions.args() + args.add(ctx.file.jar) + args.add(out_jar) + args.add_all(ctx.attr.paths_to_strip) + ctx.actions.run( + outputs = [out_jar], + inputs = [ctx.file.jar], + arguments = [args], + executable = ctx.executable._jar_stripper, + ) + + return [ + DefaultInfo( + files = depset([out_jar]), + # Workaround for https://github.com/bazelbuild/bazel/issues/15043. + runfiles = ctx.runfiles(files = [out_jar]), + ), + coverage_common.instrumented_files_info( + ctx, + dependency_attributes = ["jar"], + ), + ] + +strip_jar = rule( + implementation = _strip_jar, + attrs = { + "out": attr.output(), + "jar": attr.label( + mandatory = True, + allow_single_file = [".jar"], + ), + "paths_to_strip": attr.string_list(), + "_jar_stripper": attr.label( + default = "//bazel/tools/java:JarStripper", + cfg = "exec", + executable = True, + ), + }, +) diff --git a/bazel/tools/java/BUILD.bazel b/bazel/tools/java/BUILD.bazel new file mode 100644 index 00000000..becfe759 --- /dev/null +++ b/bazel/tools/java/BUILD.bazel @@ -0,0 +1,13 @@ +java_library( + name = "fuzz_target_test_wrapper", + srcs = ["com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java"], + visibility = ["//:__subpackages__"], + deps = ["@bazel_tools//tools/java/runfiles"], +) + +java_binary( + name = "JarStripper", + srcs = ["com/code_intelligence/jazzer/tools/JarStripper.java"], + main_class = "com.code_intelligence.jazzer.tools.JarStripper", + visibility = ["//visibility:public"], +) diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java new file mode 100644 index 00000000..107d8526 --- /dev/null +++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/FuzzTargetTestWrapper.java @@ -0,0 +1,245 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.code_intelligence.jazzer.tools; + +import com.google.devtools.build.runfiles.Runfiles; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +public class FuzzTargetTestWrapper { + private static final boolean JAZZER_CI = "1".equals(System.getenv("JAZZER_CI")); + + public static void main(String[] args) { + Runfiles runfiles; + String driverActualPath; + String apiActualPath; + String jarActualPath; + boolean verifyCrashInput; + boolean verifyCrashReproducer; + boolean expectCrash; + Set<String> expectedFindings; + List<String> arguments; + try { + runfiles = Runfiles.create(); + driverActualPath = lookUpRunfile(runfiles, args[0]); + apiActualPath = lookUpRunfile(runfiles, args[1]); + jarActualPath = lookUpRunfile(runfiles, args[2]); + verifyCrashInput = Boolean.parseBoolean(args[3]); + verifyCrashReproducer = Boolean.parseBoolean(args[4]); + expectCrash = Boolean.parseBoolean(args[5]); + expectedFindings = + Arrays.stream(args[6].split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toSet()); + // Map all files/dirs to real location + arguments = + Arrays.stream(args) + .skip(7) + .map(arg -> arg.startsWith("-") ? arg : lookUpRunfileWithFallback(runfiles, arg)) + .collect(Collectors.toList()); + } catch (IOException | ArrayIndexOutOfBoundsException e) { + e.printStackTrace(); + System.exit(1); + return; + } + + ProcessBuilder processBuilder = new ProcessBuilder(); + Map<String, String> environment = processBuilder.environment(); + // Ensure that Jazzer can find its runfiles. + environment.putAll(runfiles.getEnvVars()); + + // Crashes will be available as test outputs. These are cleared on the next run, + // so this is only useful for examples. + String outputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + + List<String> command = new ArrayList<>(); + command.add(driverActualPath); + command.add(String.format("-artifact_prefix=%s/", outputDir)); + command.add(String.format("--reproducer_path=%s", outputDir)); + command.add(String.format("--cp=%s", jarActualPath)); + if (System.getenv("JAZZER_NO_EXPLICIT_SEED") == null) { + command.add("-seed=2735196724"); + } + command.addAll(arguments); + + processBuilder.inheritIO(); + if (JAZZER_CI) { + // Make JVM error reports available in test outputs. + processBuilder.environment().put( + "JAVA_TOOL_OPTIONS", String.format("-XX:ErrorFile=%s/hs_err_pid%%p.log", outputDir)); + } + processBuilder.command(command); + + try { + int exitCode = processBuilder.start().waitFor(); + if (!expectCrash) { + if (exitCode != 0) { + System.err.printf( + "Did not expect a crash, but Jazzer exited with exit code %d%n", exitCode); + System.exit(1); + } + System.exit(0); + } + // Assert that we either found a crash in Java (exit code 77) or a sanitizer crash (exit code + // 76). + if (exitCode != 76 && exitCode != 77) { + System.err.printf("Did expect a crash, but Jazzer exited with exit code %d%n", exitCode); + System.exit(1); + } + String[] outputFiles = new File(outputDir).list(); + if (outputFiles == null) { + System.err.printf("Jazzer did not write a crashing input into %s%n", outputDir); + System.exit(1); + } + // Verify that libFuzzer dumped a crashing input. + if (JAZZER_CI && verifyCrashInput + && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("crash-"))) { + System.err.printf("No crashing input found in %s%n", outputDir); + System.exit(1); + } + // Verify that libFuzzer dumped a crash reproducer. + if (JAZZER_CI && verifyCrashReproducer + && Arrays.stream(outputFiles).noneMatch(name -> name.startsWith("Crash_"))) { + System.err.printf("No crash reproducer found in %s%n", outputDir); + System.exit(1); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + System.exit(1); + } + + if (JAZZER_CI && verifyCrashReproducer) { + try { + verifyCrashReproducer( + outputDir, driverActualPath, apiActualPath, jarActualPath, expectedFindings); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + System.exit(0); + } + + // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path. + private static String lookUpRunfile(Runfiles runfiles, String rootpath) { + return runfiles.rlocation(rlocationPath(rootpath)); + } + + // Looks up a Bazel "rootpath" in this binary's runfiles and returns the resulting path if it + // exists. If not, returns the original path unmodified. + private static String lookUpRunfileWithFallback(Runfiles runfiles, String rootpath) { + String candidatePath; + try { + candidatePath = lookUpRunfile(runfiles, rootpath); + } catch (IllegalArgumentException unused) { + // The argument to Runfiles.rlocation had an invalid format, which indicates that rootpath + // is not a Bazel "rootpath" but a user-supplied path that should be returned unchanged. + return rootpath; + } + if (new File(candidatePath).exists()) { + return candidatePath; + } else { + return rootpath; + } + } + + // Turns the result of Bazel's `$(rootpath ...)` into the correct format for rlocation. + private static String rlocationPath(String rootpath) { + if (rootpath.startsWith("external/")) { + return rootpath.substring("external/".length()); + } else { + return "jazzer/" + rootpath; + } + } + + private static void verifyCrashReproducer(String outputDir, String driver, String api, String jar, + Set<String> expectedFindings) throws Exception { + File source = + Files.list(Paths.get(outputDir)) + .filter(f -> f.toFile().getName().endsWith(".java")) + // Verify the crash reproducer that was created last in order to reproduce the last + // crash when using --keep_going. + .max(Comparator.comparingLong(p -> p.toFile().lastModified())) + .map(Path::toFile) + .orElseThrow( + () -> new IllegalStateException("Could not find crash reproducer in " + outputDir)); + String crashReproducer = compile(source, driver, api, jar); + execute(crashReproducer, outputDir, expectedFindings); + } + + private static String compile(File source, String driver, String api, String jar) + throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(source); + List<String> options = + Arrays.asList("-classpath", String.join(File.pathSeparator, driver, api, jar)); + System.out.printf( + "Compile crash reproducer %s with options %s%n", source.getAbsolutePath(), options); + CompilationTask task = + compiler.getTask(null, fileManager, null, options, null, compilationUnits); + if (!task.call()) { + throw new IllegalStateException("Could not compile crash reproducer " + source); + } + return source.getName().substring(0, source.getName().indexOf(".")); + } + } + + private static void execute(String classFile, String outputDir, Set<String> expectedFindings) + throws IOException, ReflectiveOperationException { + try { + System.out.printf("Execute crash reproducer %s%n", classFile); + URLClassLoader classLoader = + new URLClassLoader(new URL[] {new URL("file://" + outputDir + "/")}); + Class<?> crashReproducerClass = classLoader.loadClass(classFile); + Method main = crashReproducerClass.getMethod("main", String[].class); + System.setProperty("jazzer.is_reproducer", "true"); + main.invoke(null, new Object[] {new String[] {}}); + if (!expectedFindings.isEmpty()) { + throw new IllegalStateException("Expected crash with any of " + + String.join(", ", expectedFindings) + " not reproduced by " + classFile); + } + System.out.println("Reproducer finished successfully without finding"); + } catch (InvocationTargetException e) { + // expect the invocation to fail with the prescribed finding + Throwable finding = e.getCause(); + if (expectedFindings.isEmpty()) { + throw new IllegalStateException("Did not expect " + classFile + " to crash", finding); + } else if (expectedFindings.contains(finding.getClass().getName())) { + System.out.printf("Reproduced exception \"%s\"%n", finding.getMessage()); + } else { + throw new IllegalStateException( + classFile + " did not crash with any of " + String.join(", ", expectedFindings), + finding); + } + } + } +} diff --git a/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java new file mode 100644 index 00000000..2a567c68 --- /dev/null +++ b/bazel/tools/java/com/code_intelligence/jazzer/tools/JarStripper.java @@ -0,0 +1,94 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.tools; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JarStripper { + private static final Map<String, String> ZIP_FS_PROPERTIES = new HashMap<>(); + static { + // We copy the input to the output path before modifying, so don't try to create a new file at + // that path if something went wrong. + ZIP_FS_PROPERTIES.put("create", "false"); + } + + public static void main(String[] args) { + if (args.length < 2) { + System.err.println( + "Hermetically removes files and directories from .jar files by relative paths."); + System.err.println("Usage: in.jar out.jar [relative path]..."); + System.exit(1); + } + + Path inFile = Paths.get(args[0]); + Path outFile = Paths.get(args[1]); + Iterable<String> pathsToDelete = + Collections.unmodifiableList(Arrays.stream(args).skip(2).collect(Collectors.toList())); + + try { + Files.copy(inFile, outFile); + if (!outFile.toFile().setWritable(true)) { + System.err.printf("Failed to make %s writable", outFile); + System.exit(1); + } + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + URI outUri = null; + try { + outUri = new URI("jar", outFile.toUri().toString(), null); + } catch (URISyntaxException e) { + e.printStackTrace(); + System.exit(1); + } + + // Ensure that the ZipFileSystem uses a system-independent time zone for mtimes. + // https://github.com/openjdk/jdk/blob/4d64076058a4ec5df101b06572195ed5fdee6f64/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java#L241 + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + + try (FileSystem zipFs = FileSystems.newFileSystem(outUri, ZIP_FS_PROPERTIES)) { + for (String pathToDelete : pathsToDelete) { + // Visit files before the directory they are contained in by sorting in reverse order. + try (Stream<Path> walk = Files.walk(zipFs.getPath(pathToDelete))) { + Iterable<Path> subpaths = + walk.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); + for (Path subpath : subpaths) { + Files.delete(subpath); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/bazelisk-linux-amd64 b/bazelisk-linux-amd64 Binary files differdeleted file mode 100755 index 22e49af1..00000000 --- a/bazelisk-linux-amd64 +++ /dev/null diff --git a/deploy/BUILD.bazel b/deploy/BUILD.bazel index 29f9a5a1..7cceeae5 100644 --- a/deploy/BUILD.bazel +++ b/deploy/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_jvm_external//:defs.bzl", "java_export") load("//:maven.bzl", "JAZZER_API_COORDINATES") # To publish a new release of the Jazzer API to Maven, run: -# bazel run --config=maven --define "maven_user=..." --define "maven_password=..." --define gpg_sign=true //:api.publish -# Build //:api-docs.jar to generate javadocs for the API. +# bazel run --config=maven --define "maven_user=..." --define "maven_password=..." --define gpg_sign=true //deploy:api.publish +# Build //deploy:api-docs to generate javadocs for the API. java_export( name = "api", maven_coordinates = JAZZER_API_COORDINATES, diff --git a/docker/jazzer-autofuzz/entrypoint.sh b/docker/jazzer-autofuzz/entrypoint.sh index 78c57f71..6c17f125 100755 --- a/docker/jazzer-autofuzz/entrypoint.sh +++ b/docker/jazzer-autofuzz/entrypoint.sh @@ -17,8 +17,6 @@ set -e CP="$(/app/coursier.jar fetch --classpath "$1")" /app/jazzer_driver \ - -artifact_prefix=/fuzzing/ \ - --reproducer_path=/fuzzing \ --cp="$CP" \ --autofuzz="$2" \ "${@:3}" diff --git a/docker/jazzer/Dockerfile b/docker/jazzer/Dockerfile index 56787be7..bddfcb53 100644 --- a/docker/jazzer/Dockerfile +++ b/docker/jazzer/Dockerfile @@ -15,20 +15,27 @@ FROM ubuntu:20.04 AS builder ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y git python3 python-is-python3 openjdk-11-jdk-headless +RUN apt-get update && apt-get install -y curl git python3 python-is-python3 openjdk-11-jdk-headless WORKDIR /root -RUN git clone --depth=1 https://github.com/CodeIntelligenceTesting/jazzer.git && \ +RUN curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.11.0/bazelisk-linux-amd64 -o /usr/bin/bazelisk && \ + chmod +x /usr/bin/bazelisk && \ + git clone --depth=1 https://github.com/CodeIntelligenceTesting/jazzer.git && \ cd jazzer && \ # The LLVM toolchain requires ld and ld.gold to exist, but does not use them. touch /usr/bin/ld && \ touch /usr/bin/ld.gold && \ BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 \ - ./bazelisk-linux-amd64 build --config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux \ - //agent:jazzer_agent_deploy.jar //driver:jazzer_driver + bazelisk build --config=toolchain --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux \ + //agent:jazzer_agent_deploy //driver:jazzer_driver -FROM gcr.io/distroless/java +# :debug includes a busybox shell, which is needed for libFuzzer's use of system() for e.g. the +# -fork and -minimize_crash commands. +FROM gcr.io/distroless/java:debug COPY --from=builder /root/jazzer/bazel-bin/agent/jazzer_agent_deploy.jar /root/jazzer/bazel-bin/driver/jazzer_driver /app/ +# system() expects the shell at /bin/sh, but the image has it at /busybox/sh. We create a symlink, +# but have to use the long form as a simple RUN <command> also requires /bin/sh. +RUN ["/busybox/sh", "-c", "ln -s /busybox/sh /bin/sh"] WORKDIR /fuzzing -ENTRYPOINT [ "/app/jazzer_driver", "-artifact_prefix=/fuzzing/", "--reproducer_path=/fuzzing" ] +ENTRYPOINT [ "/app/jazzer_driver" ] diff --git a/driver/BUILD.bazel b/driver/BUILD.bazel index becd4fe1..2d503cce 100644 --- a/driver/BUILD.bazel +++ b/driver/BUILD.bazel @@ -1,130 +1,103 @@ -load("//bazel:cc.bzl", "cc_17_library") +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") cc_library( - name = "sanitizer_hooks_with_pc", - srcs = ["sanitizer_hooks_with_pc.cpp"], - hdrs = ["sanitizer_hooks_with_pc.h"], - linkstatic = True, -) - -cc_test( - name = "sanitizer_hooks_with_pc_test", - size = "small", - srcs = ["sanitizer_hooks_with_pc_test.cpp"], - deps = [ - ":sanitizer_hooks_with_pc", - "@googletest//:gtest", - "@googletest//:gtest_main", - ], -) - -cc_library( - name = "fuzzed_data_provider", - srcs = [ - "fuzzed_data_provider.cpp", - ], - hdrs = [ - "fuzzed_data_provider.h", - ], - visibility = [ - "//agent/src/main/native/com/code_intelligence/jazzer/replay:__pkg__", - ], + name = "jazzer_main", + srcs = ["jazzer_main.cpp"], deps = [ - "@com_google_absl//absl/strings:str_format", - "@fmeum_rules_jni//jni", + ":jvm_tooling_lib", + "@com_google_absl//absl/strings", + "@fmeum_rules_jni//jni:libjvm", + "@jazzer_com_github_gflags_gflags//:gflags", ], ) cc_library( name = "jvm_tooling_lib", - srcs = [ - "coverage_tracker.cpp", - "fuzz_target_runner.cpp", - "java_reproducer.cpp", - "java_reproducer.h", - "java_reproducer_templates.h", - "jvm_tooling.cpp", - "libfuzzer_callbacks.cpp", - "libfuzzer_callbacks.h", - "libfuzzer_driver.cpp", - "signal_handler.cpp", - "signal_handler.h", - "utils.cpp", - "utils.h", - ], - hdrs = [ - "coverage_tracker.h", - "fuzz_target_runner.h", - "fuzzed_data_provider.h", - "jvm_tooling.h", - "libfuzzer_driver.h", - ], - linkopts = select({ - "@platforms//os:windows": [], - "//conditions:default": ["-ldl"], - }), - # Needs to be linked statically for JNI_OnLoad_jazzer_initialize to be found - # by the JVM. - linkstatic = True, - local_defines = select({ - # Windows does not have SIGUSR1, which triggers a graceful exit of - # libFuzzer. Instead, trigger a hard exit. - "@platforms//os:windows": ["SIGUSR1=SIGTERM"], - "//conditions:default": [], - }), + srcs = ["jvm_tooling.cpp"], + hdrs = ["jvm_tooling.h"], tags = [ # Should be built through the cc_17_library driver_lib. "manual", ], - visibility = ["//visibility:public"], deps = [ - ":fuzzed_data_provider", - ":sanitizer_hooks_with_pc", "@bazel_tools//tools/cpp/runfiles", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", - "@com_google_glog//:glog", - "@fmeum_rules_jni//jni:libjvm", + "@fmeum_rules_jni//jni", "@jazzer_com_github_gflags_gflags//:gflags", ], ) -cc_17_library( - name = "driver_lib", - srcs = [ - "libfuzzer_fuzz_target.cpp", - ], - linkstatic = True, - deps = [ - ":jvm_tooling_lib", - "@jazzer_libfuzzer//:libFuzzer", - ], +DYNAMIC_SYMBOLS_TO_EXPORT = [ + "__sanitizer_cov_8bit_counters_init", + "__sanitizer_cov_pcs_init", + "__sanitizer_cov_trace_cmp1", + "__sanitizer_cov_trace_cmp4", + "__sanitizer_cov_trace_cmp4", + "__sanitizer_cov_trace_cmp8", + "__sanitizer_cov_trace_const_cmp1", + "__sanitizer_cov_trace_const_cmp4", + "__sanitizer_cov_trace_const_cmp4", + "__sanitizer_cov_trace_const_cmp8", + "__sanitizer_cov_trace_div4", + "__sanitizer_cov_trace_div8", + "__sanitizer_cov_trace_gep", + "__sanitizer_cov_trace_pc_indir", + "__sanitizer_cov_trace_switch", + "__sanitizer_weak_hook_memcmp", + "__sanitizer_weak_hook_memmem", + "__sanitizer_weak_hook_strcasecmp", + "__sanitizer_weak_hook_strcasestr", + "__sanitizer_weak_hook_strcmp", + "__sanitizer_weak_hook_strncasecmp", + "__sanitizer_weak_hook_strncmp", + "__sanitizer_weak_hook_strstr", + "bcmp", + "jazzer_initialize_native_hooks", + "memcmp", + "memmem", + "strcasecmp", + "strcasestr", + "strcmp", + "strncasecmp", + "strncmp", + "strstr", +] + +cc_library( + name = "native_fuzzer_hooks", + srcs = ["native_fuzzer_hooks.c"], + linkopts = select({ + "@platforms//os:linux": [ + "-Wl,--export-dynamic-symbol=" + symbol + for symbol in DYNAMIC_SYMBOLS_TO_EXPORT + ] + [ + "-ldl", + ], + "@platforms//os:macos": [ + "-rdynamic", + "-ldl", + ], + "//conditions:default": [], + }), + target_compatible_with = SKIP_ON_WINDOWS, + deps = ["//driver/src/main/native/com/code_intelligence/jazzer/driver:sanitizer_hooks_with_pc"], alwayslink = True, ) cc_binary( name = "jazzer_driver", - srcs = [ - # Defines symbols otherwise defined by sanitizers to prevent linker - # errors and print JVM stack traces. - # Windows-compatible replacement for __attribute__((weak)). - "sanitizer_symbols.cpp", - ], data = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", ], linkopts = select({ - "@platforms//os:windows": [], - "//conditions:default": [ - "-rdynamic", - ], - }) + select({ "//:clang_on_linux": ["-fuse-ld=lld"], "//conditions:default": [], }), linkstatic = True, visibility = ["//visibility:public"], - deps = [":driver_lib"], + deps = [":jazzer_main"], ) alias( @@ -140,10 +113,9 @@ alias( cc_binary( name = "jazzer_driver_asan", data = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", ], - linkopts = [ - ] + select({ + linkopts = select({ "@platforms//os:windows": [ # Sanitizer runtimes have to be linked manually on Windows: # https://devblogs.microsoft.com/cppblog/addresssanitizer-asan-for-windows-with-msvc/ @@ -153,7 +125,6 @@ cc_binary( "//conditions:default": [ "-fsanitize=address", "-static-libsan", - "-rdynamic", ], }) + select({ "//:clang_on_linux": ["-fuse-ld=lld"], @@ -161,21 +132,23 @@ cc_binary( }), linkstatic = True, visibility = ["//visibility:public"], - deps = [":driver_lib"] + select({ + deps = [":jazzer_main"] + select({ # There is no static ASan runtime on macOS, so link to the dynamic # runtime library if on macOS and using the toolchain. ":using_toolchain_on_osx": ["@llvm_toolchain_llvm//:macos_asan_dynamic"], "//conditions:default": [], + }) + select({ + "@platforms//os:windows": [], + "//conditions:default": [":native_fuzzer_hooks"], }), ) cc_binary( name = "jazzer_driver_ubsan", data = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", ], - linkopts = [ - ] + select({ + linkopts = select({ "@platforms//os:windows": [ # Sanitizer runtimes have to be linked manually on Windows: # https://devblogs.microsoft.com/cppblog/addresssanitizer-asan-for-windows-with-msvc/ @@ -187,7 +160,6 @@ cc_binary( # Link UBSan statically, even on macOS. "-static-libsan", "-fsanitize-link-c++-runtime", - "-rdynamic", ], }) + select({ "//:clang_on_linux": ["-fuse-ld=lld"], @@ -195,32 +167,26 @@ cc_binary( }), linkstatic = True, visibility = ["//visibility:public"], - deps = [":driver_lib"], + deps = [ + ":jazzer_main", + ] + select({ + "@platforms//os:windows": [], + "//conditions:default": [":native_fuzzer_hooks"], + }), ) cc_test( name = "jvm_tooling_test", size = "small", - srcs = [ - "jvm_tooling_test.cpp", - "sanitizer_symbols_for_tests.cpp", - ], + srcs = ["jvm_tooling_test.cpp"], args = [ "--cp=jazzer/$(rootpath //driver/testdata:fuzz_target_mocks_deploy.jar)", ], data = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", "//driver/testdata:fuzz_target_mocks_deploy.jar", ], includes = ["."], - linkopts = select({ - "@platforms//os:windows": [], - "//conditions:default": [ - # Needs to export symbols dynamically for JNI_OnLoad_jazzer_initialize - # to be found by the JVM. - "-rdynamic", - ], - }), deps = [ ":jvm_tooling_lib", ":test_main", @@ -233,21 +199,23 @@ cc_test( cc_test( name = "fuzzed_data_provider_test", size = "medium", - srcs = [ - "fuzzed_data_provider_test.cpp", - "sanitizer_symbols_for_tests.cpp", - ], + srcs = ["fuzzed_data_provider_test.cpp"], args = [ "--cp=jazzer/$(rootpath //driver/testdata:fuzz_target_mocks_deploy.jar)", ], + copts = select({ + "@platforms//os:windows": ["/std:c++17"], + "//conditions:default": ["-std=c++17"], + }), data = [ - "//agent:jazzer_agent_deploy.jar", + "//agent:jazzer_agent_deploy", "//driver/testdata:fuzz_target_mocks_deploy.jar", ], includes = ["."], deps = [ ":jvm_tooling_lib", ":test_main", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:fuzzed_data_provider", "@bazel_tools//tools/cpp/runfiles", "@googletest//:gtest", "@jazzer_com_github_gflags_gflags//:gflags", @@ -259,6 +227,7 @@ cc_library( srcs = ["test_main.cpp"], linkstatic = True, deps = [ + "@fmeum_rules_jni//jni:libjvm", "@googletest//:gtest", "@jazzer_com_github_gflags_gflags//:gflags", ], diff --git a/driver/coverage_tracker.cpp b/driver/coverage_tracker.cpp deleted file mode 100644 index 0a576085..00000000 --- a/driver/coverage_tracker.cpp +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "coverage_tracker.h" - -#include <jni.h> - -#include <algorithm> -#include <memory> -#include <stdexcept> - -#include "absl/strings/str_format.h" - -extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start, - uint8_t *end); -extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, - const uintptr_t *pcs_end); -extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries); - -constexpr auto kCoverageMapClass = - "com/code_intelligence/jazzer/runtime/CoverageMap"; -constexpr auto kByteBufferClass = "java/nio/ByteBuffer"; -constexpr auto kCoverageRecorderClass = - "com/code_intelligence/jazzer/instrumentor/CoverageRecorder"; - -// The initial size of the Java coverage map (512 counters). -constexpr std::size_t kInitialCoverageCountersBufferSize = 1u << 9u; -// The maximum size of the Java coverage map (1,048,576 counters). -// Since the memory for the coverage map needs to be allocated contiguously, -// increasing the maximum size incurs additional memory (but not runtime) -// overhead for all fuzz targets. -constexpr std::size_t kMaxCoverageCountersBufferSize = 1u << 20u; -static_assert(kMaxCoverageCountersBufferSize <= - std::numeric_limits<jint>::max()); - -namespace { -void AssertNoException(JNIEnv &env) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error( - "Java exception occurred in CoverageTracker JNI code"); - } -} -} // namespace - -namespace jazzer { - -uint8_t *CoverageTracker::counters_ = nullptr; -uint32_t *CoverageTracker::fake_instructions_ = nullptr; -PCTableEntry *CoverageTracker::pc_entries_ = nullptr; - -void CoverageTracker::Setup(JNIEnv &env) { - if (counters_ != nullptr) { - throw std::runtime_error( - "CoverageTracker::Setup must not be called more than once"); - } - JNINativeMethod coverage_tracker_native_methods[]{ - {(char *)"registerNewCoverageCounters", (char *)"()V", - (void *)&RegisterNewCoverageCounters}, - }; - jclass coverage_map = env.FindClass(kCoverageMapClass); - env.RegisterNatives(coverage_map, coverage_tracker_native_methods, 1); - - // libFuzzer requires an array containing the instruction addresses associated - // with the coverage counters registered above. Given that we are - // instrumenting Java code, we need to synthesize addresses that are known not - // to conflict with any valid instruction address in native code. Just like - // atheris we ensure there are no collisions by using the addresses of an - // allocated buffer. Note: We intentionally never deallocate the allocations - // made here as they have static lifetime and we can't guarantee they wouldn't - // be freed before libFuzzer stops using them. - constexpr std::size_t counters_size = kMaxCoverageCountersBufferSize; - counters_ = new uint8_t[counters_size]; - Clear(); - - // Never deallocated, see above. - fake_instructions_ = new uint32_t[counters_size]; - std::fill(fake_instructions_, fake_instructions_ + counters_size, 0); - - // Never deallocated, see above. - pc_entries_ = new PCTableEntry[counters_size]; - for (std::size_t i = 0; i < counters_size; ++i) { - pc_entries_[i].PC = reinterpret_cast<uintptr_t>(fake_instructions_ + i); - // TODO: Label Java PCs corresponding to functions as such. - pc_entries_[i].PCFlags = 0; - } - - // Register the first batch of coverage counters. - RegisterNewCoverageCounters(env, nullptr); -} - -void JNICALL CoverageTracker::RegisterNewCoverageCounters(JNIEnv &env, - jclass cls) { - jclass coverage_map = env.FindClass(kCoverageMapClass); - AssertNoException(env); - jfieldID counters_buffer_id = env.GetStaticFieldID( - coverage_map, "mem", absl::StrFormat("L%s;", kByteBufferClass).c_str()); - AssertNoException(env); - jobject counters_buffer = - env.GetStaticObjectField(coverage_map, counters_buffer_id); - AssertNoException(env); - - jclass byte_buffer = env.FindClass(kByteBufferClass); - AssertNoException(env); - jmethodID byte_buffer_capacity_id = - env.GetMethodID(byte_buffer, "capacity", "()I"); - AssertNoException(env); - jint old_counters_buffer_size = - env.CallIntMethod(counters_buffer, byte_buffer_capacity_id); - AssertNoException(env); - - jint new_counters_buffer_size; - if (old_counters_buffer_size == 0) { - new_counters_buffer_size = kInitialCoverageCountersBufferSize; - } else { - new_counters_buffer_size = 2 * old_counters_buffer_size; - if (new_counters_buffer_size > kMaxCoverageCountersBufferSize) { - throw std::runtime_error( - "Maximal size of the coverage counters buffer exceeded"); - } - } - - jobject new_counters_buffer = env.NewDirectByteBuffer( - static_cast<void *>(counters_), new_counters_buffer_size); - AssertNoException(env); - env.SetStaticObjectField(coverage_map, counters_buffer_id, - new_counters_buffer); - AssertNoException(env); - - // Register only the new second half of the counters buffer with libFuzzer. - __sanitizer_cov_8bit_counters_init(counters_ + old_counters_buffer_size, - counters_ + new_counters_buffer_size); - __sanitizer_cov_pcs_init( - (uintptr_t *)(pc_entries_ + old_counters_buffer_size), - (uintptr_t *)(pc_entries_ + new_counters_buffer_size)); -} - -void CoverageTracker::Clear() { - std::fill(counters_, counters_ + kMaxCoverageCountersBufferSize, 0); -} - -uint8_t *CoverageTracker::GetCoverageCounters() { return counters_; } - -void CoverageTracker::RecordInitialCoverage(JNIEnv &env) { - jclass coverage_recorder = env.FindClass(kCoverageRecorderClass); - AssertNoException(env); - jmethodID coverage_recorder_update_covered_ids_with_coverage_map = - env.GetStaticMethodID(coverage_recorder, - "updateCoveredIdsWithCoverageMap", "()V"); - AssertNoException(env); - env.CallStaticVoidMethod( - coverage_recorder, - coverage_recorder_update_covered_ids_with_coverage_map); - AssertNoException(env); -} - -void CoverageTracker::ReplayInitialCoverage(JNIEnv &env) { - jclass coverage_recorder = env.FindClass(kCoverageRecorderClass); - AssertNoException(env); - jmethodID coverage_recorder_update_covered_ids_with_coverage_map = - env.GetStaticMethodID(coverage_recorder, "replayCoveredIds", "()V"); - AssertNoException(env); - env.CallStaticVoidMethod( - coverage_recorder, - coverage_recorder_update_covered_ids_with_coverage_map); - AssertNoException(env); -} - -std::string CoverageTracker::ComputeCoverage(JNIEnv &env) { - uintptr_t *covered_pcs; - size_t num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs); - std::vector<jint> covered_edge_ids{}; - covered_edge_ids.reserve(num_covered_pcs); - const uintptr_t first_pc = pc_entries_[0].PC; - std::for_each(covered_pcs, covered_pcs + num_covered_pcs, - [&covered_edge_ids, first_pc](const uintptr_t pc) { - jint edge_id = - (pc - first_pc) / sizeof(fake_instructions_[0]); - covered_edge_ids.push_back(edge_id); - }); - delete[] covered_pcs; - - jclass coverage_recorder = env.FindClass(kCoverageRecorderClass); - AssertNoException(env); - jmethodID coverage_recorder_compute_file_coverage = env.GetStaticMethodID( - coverage_recorder, "computeFileCoverage", "([I)Ljava/lang/String;"); - AssertNoException(env); - jintArray covered_edge_ids_jni = env.NewIntArray(num_covered_pcs); - AssertNoException(env); - env.SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs, - covered_edge_ids.data()); - AssertNoException(env); - auto file_coverage_jni = (jstring)(env.CallStaticObjectMethod( - coverage_recorder, coverage_recorder_compute_file_coverage, - covered_edge_ids_jni)); - AssertNoException(env); - auto file_coverage_cstr = env.GetStringUTFChars(file_coverage_jni, nullptr); - AssertNoException(env); - std::string file_coverage(file_coverage_cstr); - env.ReleaseStringUTFChars(file_coverage_jni, file_coverage_cstr); - AssertNoException(env); - return file_coverage; -} -} // namespace jazzer diff --git a/driver/fuzz_target_runner.cpp b/driver/fuzz_target_runner.cpp deleted file mode 100644 index 934e27e1..00000000 --- a/driver/fuzz_target_runner.cpp +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "fuzz_target_runner.h" - -#include <jni.h> - -#include <fstream> -#include <iomanip> -#include <iostream> -#include <string> -#include <vector> - -#include "absl/strings/escaping.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_replace.h" -#include "absl/strings/str_split.h" -#include "absl/strings/substitute.h" -#include "coverage_tracker.h" -#include "fuzzed_data_provider.h" -#include "gflags/gflags.h" -#include "glog/logging.h" -#include "java_reproducer.h" -#include "java_reproducer_templates.h" -#include "utils.h" - -DEFINE_string( - target_class, "", - "The Java class that contains the static fuzzerTestOneInput function"); -DEFINE_string(target_args, "", - "Arguments passed to fuzzerInitialize as a String array. " - "Separated by space."); - -DEFINE_uint32(keep_going, 0, - "Continue fuzzing until N distinct exception stack traces have" - "been encountered. Defaults to exit after the first finding " - "unless --autofuzz is specified."); -DEFINE_bool(dedup, true, - "Emit a dedup token for every finding. Defaults to true and is " - "required for --keep_going and --ignore."); -DEFINE_string( - ignore, "", - "Comma-separated list of crash dedup tokens to ignore. This is useful to " - "continue fuzzing before a crash is fixed."); - -DEFINE_string(reproducer_path, ".", - "Path at which fuzzing reproducers are stored. Defaults to the " - "current directory."); -DEFINE_string(coverage_report, "", - "Path at which a coverage report is stored when the fuzzer " - "exits. If left empty, no report is generated (default)"); - -DEFINE_string(autofuzz, "", - "Fully qualified reference to a method on the classpath that " - "should be fuzzed automatically (example: System.out::println). " - "Fuzzing will continue even after a finding; specify " - "--keep_going=N to stop after N findings."); -DEFINE_string(autofuzz_ignore, "", - "Fully qualified class names of exceptions to ignore during " - "autofuzz. Separated by comma."); - -DECLARE_bool(hooks); - -constexpr auto kManifestUtilsClass = - "com/code_intelligence/jazzer/runtime/ManifestUtils"; -constexpr auto kJazzerClass = - "com/code_intelligence/jazzer/runtime/JazzerInternal"; -constexpr auto kAutofuzzFuzzTargetClass = - "com/code_intelligence/jazzer/autofuzz/FuzzTarget"; - -namespace jazzer { -// split a string on unescaped spaces -std::vector<std::string> splitOnSpace(const std::string &s) { - if (s.empty()) { - return {}; - } - - std::vector<std::string> tokens; - std::size_t token_begin = 0; - for (std::size_t i = 1; i < s.size() - 1; i++) { - // only split if the space is not escaped by a backslash "\" - if (s[i] == ' ' && s[i - 1] != '\\') { - // don't split on multiple spaces - if (i > token_begin + 1) - tokens.push_back(s.substr(token_begin, i - token_begin)); - token_begin = i + 1; - } - } - tokens.push_back(s.substr(token_begin)); - return tokens; -} - -FuzzTargetRunner::FuzzTargetRunner( - JVM &jvm, const std::vector<std::string> &additional_target_args) - : ExceptionPrinter(jvm), jvm_(jvm), ignore_tokens_() { - auto &env = jvm.GetEnv(); - if (!FLAGS_target_class.empty() && !FLAGS_autofuzz.empty()) { - std::cerr << "--target_class and --autofuzz cannot be specified together" - << std::endl; - exit(1); - } - if (!FLAGS_target_args.empty() && !FLAGS_autofuzz.empty()) { - std::cerr << "--target_args and --autofuzz cannot be specified together" - << std::endl; - exit(1); - } - if (FLAGS_autofuzz.empty() && !FLAGS_autofuzz_ignore.empty()) { - std::cerr << "--autofuzz_ignore requires --autofuzz" << std::endl; - exit(1); - } - if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) { - FLAGS_target_class = DetectFuzzTargetClass(); - } - // If automatically detecting the fuzz target class failed, we expect it as - // the value of the --target_class argument. - if (FLAGS_target_class.empty() && FLAGS_autofuzz.empty()) { - std::cerr << "Missing argument --target_class=<fuzz_target_class>" - << std::endl; - exit(1); - } - if (!FLAGS_autofuzz.empty()) { - FLAGS_target_class = kAutofuzzFuzzTargetClass; - if (FLAGS_keep_going == 0) { - FLAGS_keep_going = std::numeric_limits<gflags::uint32>::max(); - } - // Pass the method reference string as the first argument to the generic - // autofuzz fuzz target. Subseqeuent arguments are interpreted as exception - // class names that should be ignored. - FLAGS_target_args = FLAGS_autofuzz; - if (!FLAGS_autofuzz_ignore.empty()) { - FLAGS_target_args = absl::StrCat( - FLAGS_target_args, " ", - absl::StrReplaceAll(FLAGS_autofuzz_ignore, {{",", " "}})); - } - } - // Set --keep_going to its real default. - if (FLAGS_keep_going == 0) { - FLAGS_keep_going = 1; - } - if ((!FLAGS_ignore.empty() || FLAGS_keep_going > 1) && !FLAGS_dedup) { - std::cerr << "--nodedup is not supported with --ignore or --keep_going" - << std::endl; - exit(1); - } - jazzer_ = jvm.FindClass(kJazzerClass); - last_finding_ = - env.GetStaticFieldID(jazzer_, "lastFinding", "Ljava/lang/Throwable;"); - - jclass_ = jvm.FindClass(FLAGS_target_class); - // one of the following functions is required: - // public static void fuzzerTestOneInput(byte[] input) - // public static void fuzzerTestOneInput(FuzzedDataProvider data) - fuzzer_test_one_input_bytes_ = - jvm.GetStaticMethodID(jclass_, "fuzzerTestOneInput", "([B)V", false); - fuzzer_test_one_input_data_ = jvm.GetStaticMethodID( - jclass_, "fuzzerTestOneInput", - "(Lcom/code_intelligence/jazzer/api/FuzzedDataProvider;)V", false); - bool using_bytes = fuzzer_test_one_input_bytes_ != nullptr; - bool using_data = fuzzer_test_one_input_data_ != nullptr; - // Fail if none ore both of the two possible fuzzerTestOneInput versions is - // defined in the class. - if (using_bytes == using_data) { - LOG(ERROR) << FLAGS_target_class - << " must define exactly one of the following two functions:"; - LOG(ERROR) << "public static void fuzzerTestOneInput(byte[] ...)"; - LOG(ERROR) - << "public static void fuzzerTestOneInput(FuzzedDataProvider ...)"; - LOG(ERROR) << "Note: Fuzz targets returning boolean are no longer " - "supported; exceptions should be thrown instead of " - "returning true."; - exit(1); - } - - // check existence of optional methods for initialization and destruction - fuzzer_initialize_ = - jvm.GetStaticMethodID(jclass_, "fuzzerInitialize", "()V", false); - fuzzer_tear_down_ = - jvm.GetStaticMethodID(jclass_, "fuzzerTearDown", "()V", false); - fuzzer_initialize_with_args_ = jvm.GetStaticMethodID( - jclass_, "fuzzerInitialize", "([Ljava/lang/String;)V", false); - - auto fuzz_target_args_tokens = splitOnSpace(FLAGS_target_args); - fuzz_target_args_tokens.insert(fuzz_target_args_tokens.end(), - additional_target_args.begin(), - additional_target_args.end()); - - if (fuzzer_initialize_with_args_) { - // fuzzerInitialize with arguments gets priority - jclass string_class = jvm.FindClass("java/lang/String"); - jobjectArray arg_array = jvm.GetEnv().NewObjectArray( - fuzz_target_args_tokens.size(), string_class, nullptr); - for (jint i = 0; i < fuzz_target_args_tokens.size(); i++) { - jstring str = env.NewStringUTF(fuzz_target_args_tokens[i].c_str()); - env.SetObjectArrayElement(arg_array, i, str); - } - env.CallStaticObjectMethod(jclass_, fuzzer_initialize_with_args_, - arg_array); - } else if (fuzzer_initialize_) { - env.CallStaticVoidMethod(jclass_, fuzzer_initialize_); - } else { - LOG(INFO) << "did not call any fuzz target initialize functions"; - } - - if (jthrowable exception = env.ExceptionOccurred()) { - LOG(ERROR) << "== Java Exception in fuzzerInitialize: "; - LOG(ERROR) << getStackTrace(exception); - std::exit(1); - } - - if (FLAGS_hooks) { - CoverageTracker::RecordInitialCoverage(env); - } - SetUpFuzzedDataProvider(jvm_.GetEnv()); - - // Parse a comma-separated list of hex dedup tokens. - std::vector<std::string> str_ignore_tokens = - absl::StrSplit(FLAGS_ignore, ','); - for (const std::string &str_token : str_ignore_tokens) { - if (str_token.empty()) continue; - try { - ignore_tokens_.push_back(std::stoull(str_token, nullptr, 16)); - } catch (...) { - LOG(ERROR) << "Invalid dedup token (expected up to 16 hex digits): '" - << str_token << "'"; - // Don't let libFuzzer print a crash stack trace. - _Exit(1); - } - } -} - -FuzzTargetRunner::~FuzzTargetRunner() { - if (FLAGS_hooks && !FLAGS_coverage_report.empty()) { - std::string report = CoverageTracker::ComputeCoverage(jvm_.GetEnv()); - std::ofstream report_file(FLAGS_coverage_report); - if (report_file) { - report_file << report << std::flush; - } else { - LOG(ERROR) << "Failed to write coverage report to " - << FLAGS_coverage_report; - } - } - if (fuzzer_tear_down_ != nullptr) { - std::cerr << "calling fuzzer teardown function" << std::endl; - jvm_.GetEnv().CallStaticVoidMethod(jclass_, fuzzer_tear_down_); - if (jthrowable exception = jvm_.GetEnv().ExceptionOccurred()) - std::cerr << getStackTrace(exception) << std::endl; - } -} - -RunResult FuzzTargetRunner::Run(const uint8_t *data, const std::size_t size) { - auto &env = jvm_.GetEnv(); - static std::size_t run_count = 0; - if (run_count < 2) { - run_count++; - // For the first two runs only, replay the coverage recorded from static - // initializers. libFuzzer cleared the coverage map after they ran and could - // fail to see any coverage, triggering an early exit, if we don't replay it - // here. - // https://github.com/llvm/llvm-project/blob/957a5e987444d3193575d6ad8afe6c75da00d794/compiler-rt/lib/fuzzer/FuzzerLoop.cpp#L804-L809 - CoverageTracker::ReplayInitialCoverage(env); - } - if (fuzzer_test_one_input_data_ != nullptr) { - FeedFuzzedDataProvider(data, size); - env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_, - GetFuzzedDataProviderJavaObject(jvm_)); - } else { - jbyteArray byte_array = env.NewByteArray(size); - if (byte_array == nullptr) { - env.ExceptionDescribe(); - throw std::runtime_error(std::string("Cannot create byte array")); - } - env.SetByteArrayRegion(byte_array, 0, size, - reinterpret_cast<const jbyte *>(data)); - env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_bytes_, byte_array); - env.DeleteLocalRef(byte_array); - } - - const auto finding = GetFinding(); - if (finding != nullptr) { - jlong dedup_token = computeDedupToken(finding); - // Check whether this stack trace has been encountered before if - // `--keep_going` has been supplied. - if (dedup_token != 0 && FLAGS_keep_going > 1 && - std::find(ignore_tokens_.cbegin(), ignore_tokens_.cend(), - dedup_token) != ignore_tokens_.end()) { - env.DeleteLocalRef(finding); - return RunResult::kOk; - } else { - ignore_tokens_.push_back(dedup_token); - std::cout << std::endl; - std::cerr << "== Java Exception: " << getStackTrace(finding); - env.DeleteLocalRef(finding); - if (FLAGS_dedup) { - std::cout << "DEDUP_TOKEN: " << std::hex << std::setfill('0') - << std::setw(16) << dedup_token << std::endl; - } - if (ignore_tokens_.size() < static_cast<std::size_t>(FLAGS_keep_going)) { - return RunResult::kDumpAndContinue; - } else { - return RunResult::kException; - } - } - } - return RunResult::kOk; -} - -// Returns a fuzzer finding as a Throwable (or nullptr if there is none), -// clearing any JVM exceptions in the process. -jthrowable FuzzTargetRunner::GetFinding() const { - auto &env = jvm_.GetEnv(); - jthrowable unprocessed_finding = nullptr; - if (env.ExceptionCheck()) { - unprocessed_finding = env.ExceptionOccurred(); - env.ExceptionClear(); - } - // Explicitly reported findings take precedence over uncaught exceptions. - if (auto reported_finding = - (jthrowable)env.GetStaticObjectField(jazzer_, last_finding_); - reported_finding != nullptr) { - env.DeleteLocalRef(unprocessed_finding); - unprocessed_finding = reported_finding; - } - jthrowable processed_finding = preprocessException(unprocessed_finding); - env.DeleteLocalRef(unprocessed_finding); - return processed_finding; -} - -void FuzzTargetRunner::DumpReproducer(const uint8_t *data, std::size_t size) { - auto &env = jvm_.GetEnv(); - std::string base64_data; - if (fuzzer_test_one_input_data_) { - // Record the data retrieved from the FuzzedDataProvider and supply it to a - // Java-only CannedFuzzedDataProvider in the reproducer. - FeedFuzzedDataProvider(data, size); - jobject recorder = GetRecordingFuzzedDataProviderJavaObject(jvm_); - env.CallStaticVoidMethod(jclass_, fuzzer_test_one_input_data_, recorder); - const auto finding = GetFinding(); - if (finding == nullptr) { - LOG(ERROR) << "Failed to reproduce crash when rerunning with recorder"; - return; - } - base64_data = SerializeRecordingFuzzedDataProvider(jvm_, recorder); - } else { - absl::string_view data_str(reinterpret_cast<const char *>(data), size); - absl::Base64Escape(data_str, &base64_data); - } - const char *fuzz_target_call = fuzzer_test_one_input_data_ - ? kTestOneInputWithData - : kTestOneInputWithBytes; - std::string data_sha1 = jazzer::Sha1Hash(data, size); - std::string reproducer = - absl::Substitute(kBaseReproducer, data_sha1, base64_data, - FLAGS_target_class, fuzz_target_call); - std::string reproducer_filename = absl::StrFormat("Crash_%s.java", data_sha1); - std::string reproducer_full_path = absl::StrFormat( - "%s%c%s", FLAGS_reproducer_path, kPathSeparator, reproducer_filename); - std::ofstream reproducer_out(reproducer_full_path); - reproducer_out << reproducer; - std::cout << absl::StrFormat( - "reproducer_path='%s'; Java reproducer written to %s", - FLAGS_reproducer_path, reproducer_full_path) - << std::endl; -} - -std::string FuzzTargetRunner::DetectFuzzTargetClass() const { - jclass manifest_utils = jvm_.FindClass(kManifestUtilsClass); - jmethodID detect_fuzz_target_class = jvm_.GetStaticMethodID( - manifest_utils, "detectFuzzTargetClass", "()Ljava/lang/String;", true); - auto &env = jvm_.GetEnv(); - auto jni_fuzz_target_class = (jstring)(env.CallStaticObjectMethod( - manifest_utils, detect_fuzz_target_class)); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - exit(1); - } - if (jni_fuzz_target_class == nullptr) return ""; - - const char *fuzz_target_class_cstr = - env.GetStringUTFChars(jni_fuzz_target_class, nullptr); - std::string fuzz_target_class = std::string(fuzz_target_class_cstr); - env.ReleaseStringUTFChars(jni_fuzz_target_class, fuzz_target_class_cstr); - env.DeleteLocalRef(jni_fuzz_target_class); - - return fuzz_target_class; -} -} // namespace jazzer diff --git a/driver/fuzz_target_runner.h b/driver/fuzz_target_runner.h deleted file mode 100644 index 98ac794c..00000000 --- a/driver/fuzz_target_runner.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 Code Intelligence GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <jni.h> - -#include <string> -#include <vector> - -#include "jvm_tooling.h" - -namespace jazzer { - -enum class RunResult { - kOk, - kException, - kDumpAndContinue, -}; - -// Invokes the following static methods in the java fuzz target class: -// 1. On construction: -// - `public static void fuzzerInitialize()` -// OR -// - `public static void fuzzerInitialize(String[] args)` -// 2. On every call of Run(): -// - `public static void fuzzerTestOneInput(FuzzedDataProvider data)` -// OR -// - `public static void fuzzerTestOneInput(byte[] input)` -// 3. On destruction: -// - `public static void fuzzerTearDown()` -class FuzzTargetRunner : public ExceptionPrinter { - private: - const JVM &jvm_; - jclass jclass_; - jmethodID fuzzer_initialize_; - jmethodID fuzzer_initialize_with_args_; - jmethodID fuzzer_test_one_input_bytes_; - jmethodID fuzzer_test_one_input_data_; - jmethodID fuzzer_tear_down_; - jclass jazzer_; - jfieldID last_finding_; - std::vector<jlong> ignore_tokens_; - - [[nodiscard]] std::string DetectFuzzTargetClass() const; - [[nodiscard]] jthrowable GetFinding() const; - - public: - // Initializes the java fuzz target by calling `void fuzzerInitialize(...)`. - explicit FuzzTargetRunner( - JVM &jvm, const std::vector<std::string> &additional_target_args = {}); - - // Calls the fuzz target tear down function. This can be useful to join any - // Threads so that the JVM shuts down correctly. - virtual ~FuzzTargetRunner(); - - // Propagate the fuzzer input to the java fuzz target. - RunResult Run(const uint8_t *data, std::size_t size); - - void DumpReproducer(const uint8_t *data, std::size_t size); -}; - -} // namespace jazzer diff --git a/driver/fuzzed_data_provider.h b/driver/fuzzed_data_provider.h deleted file mode 100644 index 9b8faf78..00000000 --- a/driver/fuzzed_data_provider.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2021 Code Intelligence GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <jni.h> - -#include <algorithm> -#include <climits> -#include <cstddef> -#include <cstdint> -#include <cstring> -#include <initializer_list> -#include <iostream> -#include <string> -#include <type_traits> -#include <utility> -#include <vector> - -namespace jazzer { - -constexpr char kFuzzedDataProviderImplClass[] = - "com/code_intelligence/jazzer/runtime/FuzzedDataProviderImpl"; - -// Registers the native methods in FuzzedDataProvider. -void SetUpFuzzedDataProvider(JNIEnv &env); - -// Feed the FuzzedDataProvider with a new data buffer. The buffer is accessed -// by native code and not copied into the JVM, so this is cheap to call. -void FeedFuzzedDataProvider(const uint8_t *data, std::size_t size); -} // namespace jazzer diff --git a/driver/fuzzed_data_provider_test.cpp b/driver/fuzzed_data_provider_test.cpp index 210bf118..e6225b7f 100644 --- a/driver/fuzzed_data_provider_test.cpp +++ b/driver/fuzzed_data_provider_test.cpp @@ -12,101 +12,92 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "fuzzed_data_provider.h" - #include <cstddef> #include <cstdint> #include <random> -#include <sstream> #include <string> #include <vector> -#include "fuzz_target_runner.h" #include "gflags/gflags.h" #include "gtest/gtest.h" #include "jvm_tooling.h" #include "tools/cpp/runfiles/runfiles.h" DECLARE_string(cp); -DECLARE_string(jvm_args); -DECLARE_string(instrumentation_excludes); - -DECLARE_string(target_class); -DECLARE_string(target_args); +DECLARE_bool(hooks); namespace jazzer { -std::pair<std::string, std::size_t> FixUpModifiedUtf8(const uint8_t* pos, - std::size_t max_bytes, - jint max_length, - bool ascii_only, - bool stop_on_backslash); +std::pair<std::string, jint> FixUpModifiedUtf8(const uint8_t* pos, + jint max_bytes, jint max_length, + bool ascii_only, + bool stop_on_backslash); -std::pair<std::string, std::size_t> FixUpRemainingModifiedUtf8( +std::pair<std::string, jint> FixUpRemainingModifiedUtf8( const std::string& str, bool ascii_only, bool stop_on_backslash) { return FixUpModifiedUtf8(reinterpret_cast<const uint8_t*>(str.c_str()), str.length(), std::numeric_limits<jint>::max(), ascii_only, stop_on_backslash); } -// Work around the fact that size_t is unsigned long on Linux and unsigned long -// long on Windows. -std::size_t operator"" _z(unsigned long long x) { return x; } +std::pair<std::string, jint> expect(const std::string& s, jint i) { + return std::make_pair(s, i); +} using namespace std::literals::string_literals; TEST(FixUpModifiedUtf8Test, FullUtf8_ContinueOnBackslash) { - EXPECT_EQ(std::make_pair("jazzer"s, 6_z), + EXPECT_EQ(expect("jazzer"s, 6), FixUpRemainingModifiedUtf8("jazzer"s, false, false)); - EXPECT_EQ(std::make_pair("ja\xC0\x80zzer"s, 7_z), + EXPECT_EQ(expect("ja\xC0\x80zzer"s, 7), FixUpRemainingModifiedUtf8("ja\0zzer"s, false, false)); - EXPECT_EQ(std::make_pair("ja\xC0\x80\xC0\x80zzer"s, 8_z), + EXPECT_EQ(expect("ja\xC0\x80\xC0\x80zzer"s, 8), FixUpRemainingModifiedUtf8("ja\0\0zzer"s, false, false)); - EXPECT_EQ(std::make_pair("ja\\zzer"s, 7_z), + EXPECT_EQ(expect("ja\\zzer"s, 7), FixUpRemainingModifiedUtf8("ja\\zzer"s, false, false)); - EXPECT_EQ(std::make_pair("ja\\\\zzer"s, 8_z), + EXPECT_EQ(expect("ja\\\\zzer"s, 8), FixUpRemainingModifiedUtf8("ja\\\\zzer"s, false, false)); - EXPECT_EQ(std::make_pair("€ß"s, 5_z), + EXPECT_EQ(expect("€ß"s, 5), FixUpRemainingModifiedUtf8(u8"€ß"s, false, false)); } TEST(FixUpModifiedUtf8Test, AsciiOnly_ContinueOnBackslash) { - EXPECT_EQ(std::make_pair("jazzer"s, 6_z), + EXPECT_EQ(expect("jazzer"s, 6), FixUpRemainingModifiedUtf8("jazzer"s, true, false)); - EXPECT_EQ(std::make_pair("ja\xC0\x80zzer"s, 7_z), + EXPECT_EQ(expect("ja\xC0\x80zzer"s, 7), FixUpRemainingModifiedUtf8("ja\0zzer"s, true, false)); - EXPECT_EQ(std::make_pair("ja\xC0\x80\xC0\x80zzer"s, 8_z), + EXPECT_EQ(expect("ja\xC0\x80\xC0\x80zzer"s, 8), FixUpRemainingModifiedUtf8("ja\0\0zzer"s, true, false)); - EXPECT_EQ(std::make_pair("ja\\zzer"s, 7_z), + EXPECT_EQ(expect("ja\\zzer"s, 7), FixUpRemainingModifiedUtf8("ja\\zzer"s, true, false)); - EXPECT_EQ(std::make_pair("ja\\\\zzer"s, 8_z), + EXPECT_EQ(expect("ja\\\\zzer"s, 8), FixUpRemainingModifiedUtf8("ja\\\\zzer"s, true, false)); - EXPECT_EQ(std::make_pair("\x62\x02\x2C\x43\x1F"s, 5_z), + EXPECT_EQ(expect("\x62\x02\x2C\x43\x1F"s, 5), FixUpRemainingModifiedUtf8(u8"€ß"s, true, false)); } TEST(FixUpModifiedUtf8Test, FullUtf8_StopOnBackslash) { - EXPECT_EQ(std::make_pair("jazzer"s, 6_z), + EXPECT_EQ(expect("jazzer"s, 6), FixUpRemainingModifiedUtf8("jazzer"s, false, true)); - EXPECT_EQ(std::make_pair("ja\xC0\x80zzer"s, 7_z), + EXPECT_EQ(expect("ja\xC0\x80zzer"s, 7), FixUpRemainingModifiedUtf8("ja\0zzer"s, false, true)); - EXPECT_EQ(std::make_pair("ja\xC0\x80\xC0\x80zzer"s, 8_z), + EXPECT_EQ(expect("ja\xC0\x80\xC0\x80zzer"s, 8), FixUpRemainingModifiedUtf8("ja\0\0zzer"s, false, true)); - EXPECT_EQ(std::make_pair("ja"s, 4_z), + EXPECT_EQ(expect("ja"s, 4), FixUpRemainingModifiedUtf8("ja\\zzer"s, false, true)); - EXPECT_EQ(std::make_pair("ja\\zzer"s, 8_z), + EXPECT_EQ(expect("ja\\zzer"s, 8), FixUpRemainingModifiedUtf8("ja\\\\zzer"s, false, true)); } TEST(FixUpModifiedUtf8Test, AsciiOnly_StopOnBackslash) { - EXPECT_EQ(std::make_pair("jazzer"s, 6_z), + EXPECT_EQ(expect("jazzer"s, 6), FixUpRemainingModifiedUtf8("jazzer"s, true, true)); - EXPECT_EQ(std::make_pair("ja\xC0\x80zzer"s, 7_z), + EXPECT_EQ(expect("ja\xC0\x80zzer"s, 7), FixUpRemainingModifiedUtf8("ja\0zzer"s, true, true)); - EXPECT_EQ(std::make_pair("ja\xC0\x80\xC0\x80zzer"s, 8_z), + EXPECT_EQ(expect("ja\xC0\x80\xC0\x80zzer"s, 8), FixUpRemainingModifiedUtf8("ja\0\0zzer"s, true, true)); - EXPECT_EQ(std::make_pair("ja"s, 4_z), + EXPECT_EQ(expect("ja"s, 4), FixUpRemainingModifiedUtf8("ja\\zzer"s, true, true)); - EXPECT_EQ(std::make_pair("ja\\zzer"s, 8_z), + EXPECT_EQ(expect("ja\\zzer"s, 8), FixUpRemainingModifiedUtf8("ja\\\\zzer"s, true, true)); } @@ -116,7 +107,7 @@ class FuzzedDataProviderTest : public ::testing::Test { // process, so we set up a single JVM instance for this test binary which gets // destroyed after all tests in this test suite have finished. static void SetUpTestCase() { - FLAGS_instrumentation_excludes = "**"; + FLAGS_hooks = false; using ::bazel::tools::cpp::runfiles::Runfiles; Runfiles* runfiles = Runfiles::CreateForTest(); FLAGS_cp = runfiles->Rlocation(FLAGS_cp); @@ -131,135 +122,17 @@ class FuzzedDataProviderTest : public ::testing::Test { std::unique_ptr<JVM> FuzzedDataProviderTest::jvm_ = nullptr; -// see testdata/test/FuzzTargetWithDataProvider.java for the implementation -// of the fuzz target that asserts that the correct values are received from -// the data provider. -const uint8_t kInput[] = { - // Bytes read from the start - 0x01, 0x02, // consumeBytes(2): {0x01, 0x02} - - 'j', 'a', 'z', 'z', 'e', 'r', // consumeString(6): "jazzer" - 'j', 'a', 0x00, 'z', 'e', 'r', // consumeString(6): "ja\u0000zer" - 0xE2, 0x82, 0xAC, 0xC3, 0x9F, // consumeString(2): "€ẞ" - - 'j', 'a', 'z', 'z', 'e', 'r', // consumeAsciiString(6): "jazzer" - 'j', 'a', 0x00, 'z', 'e', 'r', // consumeAsciiString(6): "ja\u0000zer" - 0xE2, 0x82, 0xAC, 0xC3, - 0x9F, // consumeAsciiString(5): "\u0062\u0002\u002C\u0043\u001F" - - false, false, true, false, - true, // consumeBooleans(5): { false, false, true, false, true } - 0xEF, 0xDC, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0x10, 0x32, 0x54, 0x76, - 0x98, 0xBA, 0xDC, - 0xFE, // consumeLongs(2): { 0x0123456789ABCDEF, 0xFEDCBA9876543210 } - - 0x78, 0x56, 0x34, 0x12, // consumeInts(3): { 0x12345678 } - 0x56, 0x34, 0x12, // consumeLong(): - - // Bytes read from the end - 0x02, 0x03, 0x02, 0x04, // 4x pickValue in array with five elements - - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 10, // -max for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 9, // max for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 8, // -min for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 7, // min for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 6, // -denorm_min for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 5, // denorm_min for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 4, // NaN for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 3, // -infinity for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 2, // infinity for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 1, // -0.0 for next consumeDouble - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, - 0x78, // consumed but unused by consumeDouble() - 0, // 0.0 for next consumeDouble - - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 10, // -max for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 9, // max for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 8, // -min for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 7, // min for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 6, // -denorm_min for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 5, // denorm_min for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 4, // NaN for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 3, // -infinity for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 2, // infinity for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 1, // -0.0 for next consumeFloat - 0x12, 0x34, 0x56, 0x78, 0x90, // consumed but unused by consumeFloat() - 0, // 0.0 for next consumeFloat - - 0x88, 0xAB, 0x61, 0xCB, 0x32, 0xEB, 0x30, - 0xF9, // consumeDouble(13.37, 31.337): 30.859126145478349 (small range) - 0x51, 0xF6, 0x1F, - 0x3A, // consumeFloat(123.0, 777.0): 271.49084 (small range) - 0x11, 0x4D, 0xFD, 0x54, 0xD6, 0x3D, 0x43, 0x73, - 0x39, // consumeRegularDouble(): 8.0940194040236032e+307 - 0x16, 0xCF, 0x3D, 0x29, 0x4A, // consumeRegularFloat(): -2.8546307e+38 - - 0x61, 0xCB, 0x32, 0xEB, 0x30, 0xF9, 0x51, - 0xF6, // consumeProbabilityDouble(): 0.96218831486039413 - 0x1F, 0x3A, 0x11, 0x4D, // consumeProbabilityFloat(): 0.30104411 - 0xFD, 0x54, 0xD6, 0x3D, 0x43, 0x73, 0x39, - 0x16, // consumeProbabilityDouble(): 0.086814121166605432 - 0xCF, 0x3D, 0x29, 0x4A, // consumeProbabilityFloat(): 0.28969181 - - 0x01, // consumeInt(0x12345678, 0x12345679): 0x12345679 - 0x78, // consumeInt(-0x12345678, -0x12345600): -0x12345600 - 0x78, 0x56, 0x34, 0x12, // consumeInt(): 0x12345678 - - 0x02, // consumeByte(0x12, 0x22): 0x14 - 0x7F, // consumeByte(): 0x7F - - 0x01, // consumeBool(): true -}; - -TEST_F(FuzzedDataProviderTest, FuzzTargetWithDataProvider) { - FLAGS_target_class = "test/FuzzTargetWithDataProvider"; - FLAGS_target_args = ""; - FuzzTargetRunner fuzz_target_runner(*jvm_); - - ASSERT_EQ(RunResult::kOk, fuzz_target_runner.Run(kInput, sizeof(kInput))); -} - constexpr std::size_t kValidModifiedUtf8NumRuns = 10000; constexpr std::size_t kValidModifiedUtf8NumBytes = 100000; constexpr uint32_t kValidModifiedUtf8Seed = 0x12345678; TEST_F(FuzzedDataProviderTest, InvalidModifiedUtf8AfterFixup) { - auto modified_utf8_validator = jvm_->FindClass("test.ModifiedUtf8Encoder"); + auto& env = jvm_->GetEnv(); + auto modified_utf8_validator = env.FindClass("test/ModifiedUtf8Encoder"); ASSERT_NE(nullptr, modified_utf8_validator); - auto string_to_modified_utf_bytes = jvm_->GetStaticMethodID( + auto string_to_modified_utf_bytes = env.GetStaticMethodID( modified_utf8_validator, "encode", "(Ljava/lang/String;)[B"); ASSERT_NE(nullptr, string_to_modified_utf_bytes); - auto& env = jvm_->GetEnv(); auto random_bytes = std::vector<uint8_t>(kValidModifiedUtf8NumBytes); auto random = std::mt19937(kValidModifiedUtf8Seed); for (bool ascii_only : {false, true}) { diff --git a/driver/java_reproducer.cpp b/driver/java_reproducer.cpp deleted file mode 100644 index ed4c6755..00000000 --- a/driver/java_reproducer.cpp +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "java_reproducer.h" - -#include "fuzzed_data_provider.h" -#include "jvm_tooling.h" - -namespace { -const char kRecordingFuzzedDataProviderClass[] = - "com/code_intelligence/jazzer/runtime/RecordingFuzzedDataProvider"; -} - -namespace jazzer { -jobject GetFuzzedDataProviderJavaObject(const JVM &jvm) { - static jobject java_object = nullptr; - if (java_object == nullptr) { - jclass java_class = jvm.FindClass(kFuzzedDataProviderImplClass); - jmethodID java_constructor = jvm.GetMethodID(java_class, "<init>", "()V"); - jobject local_ref = jvm.GetEnv().NewObject(java_class, java_constructor); - // We leak a global reference here as it will be used until JVM exit. - java_object = jvm.GetEnv().NewGlobalRef(local_ref); - } - return java_object; -} - -jobject GetRecordingFuzzedDataProviderJavaObject(const JVM &jvm) { - auto &env = jvm.GetEnv(); - jclass java_class = jvm.FindClass(kRecordingFuzzedDataProviderClass); - jmethodID java_make_proxy = jvm.GetStaticMethodID( - java_class, "makeFuzzedDataProviderProxy", - "()Lcom/code_intelligence/jazzer/api/FuzzedDataProvider;", true); - jobject local_ref = env.CallStaticObjectMethod(java_class, java_make_proxy); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - exit(1); - } - // This global reference is deleted in SerializeRecordingFuzzedDataProvider. - jobject global_ref = env.NewGlobalRef(local_ref); - env.DeleteLocalRef(local_ref); - return global_ref; -} - -std::string SerializeRecordingFuzzedDataProvider(const JVM &jvm, - jobject recorder) { - auto &env = jvm.GetEnv(); - jclass java_class = jvm.FindClass(kRecordingFuzzedDataProviderClass); - jmethodID java_serialize = - jvm.GetStaticMethodID(java_class, "serializeFuzzedDataProviderProxy", - "(Lcom/code_intelligence/jazzer/api/" - "FuzzedDataProvider;)Ljava/lang/String;", - true); - auto serialized_recorder = - (jstring)env.CallStaticObjectMethod(java_class, java_serialize, recorder); - env.DeleteLocalRef(java_class); - env.DeleteGlobalRef(recorder); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - exit(1); - } - const char *serialized_recorder_cstr = - env.GetStringUTFChars(serialized_recorder, nullptr); - std::string out(serialized_recorder_cstr); - env.ReleaseStringUTFChars(serialized_recorder, serialized_recorder_cstr); - env.DeleteLocalRef(serialized_recorder); - return out; -} -} // namespace jazzer diff --git a/driver/java_reproducer.h b/driver/java_reproducer.h deleted file mode 100644 index b3202b14..00000000 --- a/driver/java_reproducer.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2021 Code Intelligence GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "jvm_tooling.h" - -namespace jazzer { -// Gets the single global reference to a Java FuzzedDataProvider object. The -// object itself doesn't hold any state and only exists to make the UX better by -// providing it as an argument to the fuzz target instead of relying on static -// calls. -jobject GetFuzzedDataProviderJavaObject(const JVM &jvm); - -jobject GetRecordingFuzzedDataProviderJavaObject(const JVM &jvm); - -std::string SerializeRecordingFuzzedDataProvider(const JVM &jvm, - jobject recorder); -} // namespace jazzer diff --git a/driver/java_reproducer_templates.h b/driver/java_reproducer_templates.h deleted file mode 100644 index 7d58e1fa..00000000 --- a/driver/java_reproducer_templates.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021 Code Intelligence GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <string> - -constexpr const char *kBaseReproducer = - R"java(import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class Crash_$0 { - static final String base64Bytes = "$1"; - - public static void main(String[] args) { - ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); - try { - Method fuzzerInitialize = $2.class.getMethod("fuzzerInitialize"); - fuzzerInitialize.invoke(null); - } catch (NoSuchMethodException ignored) { - try { - Method fuzzerInitialize = $2.class.getMethod("fuzzerInitialize", String[].class); - fuzzerInitialize.invoke(null, (Object) args); - } catch (NoSuchMethodException ignored1) { - } catch (IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - System.exit(1); - } - } catch (IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - System.exit(1); - } - $3 - $2.fuzzerTestOneInput(input); - } -} -)java"; - -constexpr const char *kTestOneInputWithBytes = - "byte[] input = java.util.Base64.getDecoder().decode(base64Bytes);"; - -constexpr const char *kTestOneInputWithData = - "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider input = new " - "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider(base64Bytes)" - ";"; diff --git a/driver/jazzer_main.cpp b/driver/jazzer_main.cpp new file mode 100644 index 00000000..c72e111f --- /dev/null +++ b/driver/jazzer_main.cpp @@ -0,0 +1,151 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + * Jazzer's native main function, which: + * 1. defines default settings for ASan and UBSan; + * 2. preprocesses the command-line arguments passed to libFuzzer; + * 3. starts a JVM; + * 4. passes control to the Java-part of the driver. + */ + +#include <rules_jni.h> + +#include <algorithm> +#include <iostream> +#include <memory> +#include <vector> + +#include "absl/strings/match.h" +#include "gflags/gflags.h" +#include "jvm_tooling.h" + +// Defined by glog +DECLARE_bool(log_prefix); + +namespace { +bool is_asan_active = false; +} + +extern "C" { +[[maybe_unused]] const char *__asan_default_options() { + is_asan_active = true; + // LeakSanitizer is not yet supported as it reports too many false positives + // due to how the JVM GC works. + // We use a distinguished exit code to recognize ASan crashes in tests. + // Also specify abort_on_error=0 explicitly since ASan aborts rather than + // exits on macOS by default, which would cause our exit code to be ignored. + return "abort_on_error=0,detect_leaks=0,exitcode=76"; +} + +[[maybe_unused]] const char *__ubsan_default_options() { + // We use a distinguished exit code to recognize UBSan crashes in tests. + // Also specify abort_on_error=0 explicitly since UBSan aborts rather than + // exits on macOS by default, which would cause our exit code to be ignored. + return "abort_on_error=0,exitcode=76"; +} +} + +namespace { +const std::string kUsageMessage = + R"(Test java fuzz targets using libFuzzer. Usage: + jazzer --cp=<java_class_path> --target_class=<fuzz_target_class> <libfuzzer_arguments...>)"; +const std::string kDriverClassName = + "com/code_intelligence/jazzer/driver/Driver"; + +int StartLibFuzzer(std::unique_ptr<jazzer::JVM> jvm, + std::vector<std::string> argv) { + JNIEnv &env = jvm->GetEnv(); + jclass runner = env.FindClass(kDriverClassName.c_str()); + if (runner == nullptr) { + env.ExceptionDescribe(); + return 1; + } + jmethodID startDriver = env.GetStaticMethodID(runner, "start", "([[B)I"); + if (startDriver == nullptr) { + env.ExceptionDescribe(); + return 1; + } + jclass byteArrayClass = env.FindClass("[B"); + if (byteArrayClass == nullptr) { + env.ExceptionDescribe(); + return 1; + } + jobjectArray args = env.NewObjectArray(argv.size(), byteArrayClass, nullptr); + if (args == nullptr) { + env.ExceptionDescribe(); + return 1; + } + for (jsize i = 0; i < argv.size(); ++i) { + jint len = argv[i].size(); + jbyteArray arg = env.NewByteArray(len); + if (arg == nullptr) { + env.ExceptionDescribe(); + return 1; + } + // startDriver expects UTF-8 encoded strings that are not null-terminated. + env.SetByteArrayRegion(arg, 0, len, + reinterpret_cast<const jbyte *>(argv[i].data())); + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + return 1; + } + env.SetObjectArrayElement(args, i, arg); + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + return 1; + } + env.DeleteLocalRef(arg); + } + int res = env.CallStaticIntMethod(runner, startDriver, args); + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + return 1; + } + env.DeleteLocalRef(args); + return res; +} +} // namespace + +int main(int argc, char **argv) { + gflags::SetUsageMessage(kUsageMessage); + rules_jni_init(argv[0]); + + const auto argv_end = argv + argc; + + { + // All libFuzzer flags start with a single dash, our arguments all start + // with a double dash. We can thus filter out the arguments meant for gflags + // by taking only those with a leading double dash. + std::vector<char *> our_args = {*argv}; + std::copy_if(argv, argv_end, std::back_inserter(our_args), + [](const std::string &arg) { + return absl::StartsWith(std::string(arg), "--"); + }); + int our_argc = our_args.size(); + char **our_argv = our_args.data(); + // Let gflags consume its flags, but keep them in the argument list in case + // libFuzzer forwards the command line (e.g. with -jobs or -minimize_crash). + gflags::ParseCommandLineFlags(&our_argc, &our_argv, false); + } + + if (is_asan_active) { + std::cerr << "WARN: Jazzer is not compatible with LeakSanitizer yet. Leaks " + "are not reported." + << std::endl; + } + + return StartLibFuzzer(std::unique_ptr<jazzer::JVM>(new jazzer::JVM(argv[0])), + std::vector<std::string>(argv, argv_end)); +} diff --git a/driver/jvm_tooling.cpp b/driver/jvm_tooling.cpp index 178eec02..71a5f581 100644 --- a/driver/jvm_tooling.cpp +++ b/driver/jvm_tooling.cpp @@ -14,6 +14,7 @@ #include "jvm_tooling.h" +#include <cstdlib> #include <fstream> #include <iostream> #include <memory> @@ -24,13 +25,8 @@ #include "absl/strings/str_join.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" -#include "coverage_tracker.h" #include "gflags/gflags.h" -#include "glog/logging.h" -#include "libfuzzer_callbacks.h" -#include "signal_handler.h" #include "tools/cpp/runfiles/runfiles.h" -#include "utils.h" DEFINE_string(cp, ".", "the classpath to use for fuzzing. Behaves analogously to java's " @@ -53,23 +49,30 @@ DEFINE_string(agent_path, "", "location of the fuzzing instrumentation agent"); // combined during the initialization of the JVM. DEFINE_string(instrumentation_includes, "", "list of glob patterns for classes that will be instrumented for " - "fuzzing. Separated by colon \":\""); -DEFINE_string(instrumentation_excludes, "", - "list of glob patterns for classes that will not be instrumented " - "for fuzzing. Separated by colon \":\""); + "fuzzing (separator is ':' on Linux/macOS and ';' on Windows)"); +DEFINE_string( + instrumentation_excludes, "", + "list of glob patterns for classes that will not be instrumented " + "for fuzzing (separator is ':' on Linux/macOS and ';' on Windows)"); DEFINE_string(custom_hook_includes, "", "list of glob patterns for classes that will only be " - "instrumented using custom hooks. Separated by colon \":\""); -DEFINE_string(custom_hook_excludes, "", - "list of glob patterns for classes that will not be instrumented " - "using custom hooks. Separated by colon \":\""); + "instrumented using custom hooks (separator is ':' on " + "Linux/macOS and ';' on Windows)"); +DEFINE_string( + custom_hook_excludes, "", + "list of glob patterns for classes that will not be instrumented " + "using custom hooks (separator is ':' on Linux/macOS and ';' on Windows)"); DEFINE_string(custom_hooks, "", - "list of classes containing custom instrumentation hooks. " - "Separated by colon \":\""); + "list of classes containing custom instrumentation hooks " + "(separator is ':' on Linux/macOS and ';' on Windows)"); +DEFINE_string(disabled_hooks, "", + "list of hook classes (custom or built-in) that should not be " + "loaded (separator is ':' on Linux/macOS and ';' on Windows)"); DEFINE_string( trace, "", - "list of instrumentation to perform separated by colon \":\". " + "list of instrumentation to perform separated by colon ':' on Linux/macOS " + "and ';' on Windows. " "Available options are cov, cmp, div, gep, all. These options " "correspond to the \"-fsanitize-coverage=trace-*\" flags in clang."); DEFINE_string( @@ -88,72 +91,57 @@ DEFINE_bool(hooks, true, "coverage information will be processed. This can be useful for " "running a regression test on non-instrumented bytecode."); -#ifdef _WIN32 +DEFINE_string( + target_class, "", + "The Java class that contains the static fuzzerTestOneInput function"); +DEFINE_string(target_args, "", + "Arguments passed to fuzzerInitialize as a String array. " + "Separated by space."); + +DEFINE_uint32(keep_going, 0, + "Continue fuzzing until N distinct exception stack traces have" + "been encountered. Defaults to exit after the first finding " + "unless --autofuzz is specified."); +DEFINE_bool(dedup, true, + "Emit a dedup token for every finding. Defaults to true and is " + "required for --keep_going and --ignore."); +DEFINE_string( + ignore, "", + "Comma-separated list of crash dedup tokens to ignore. This is useful to " + "continue fuzzing before a crash is fixed."); + +DEFINE_string(reproducer_path, ".", + "Path at which fuzzing reproducers are stored. Defaults to the " + "current directory."); +DEFINE_string(coverage_report, "", + "Path at which a coverage report is stored when the fuzzer " + "exits. If left empty, no report is generated (default)"); +DEFINE_string(coverage_dump, "", + "Path at which a coverage dump is stored when the fuzzer " + "exits. If left empty, no dump is generated (default)"); + +DEFINE_string(autofuzz, "", + "Fully qualified reference to a method on the classpath that " + "should be fuzzed automatically (example: System.out::println). " + "Fuzzing will continue even after a finding; specify " + "--keep_going=N to stop after N findings."); +DEFINE_string(autofuzz_ignore, "", + "Fully qualified class names of exceptions to ignore during " + "autofuzz. Separated by comma."); +DEFINE_bool(fake_pcs, false, + "No-op flag that remains for backwards compatibility only."); + +#if defined(_WIN32) || defined(_WIN64) #define ARG_SEPARATOR ";" +constexpr auto kPathSeparator = '\\'; #else #define ARG_SEPARATOR ":" +constexpr auto kPathSeparator = '/'; #endif -// Called by the agent when -// com.code_intelligence.jazzer.instrumentor.ClassInstrumentor is initialized. -// This only happens when FLAGS_hooks is true. -extern "C" JNIEXPORT jint JNICALL JNI_OnLoad_jazzer_initialize(JavaVM *vm, - void *) { - if (!FLAGS_hooks) { - LOG(ERROR) << "JNI_OnLoad_jazzer_initialize called with --nohooks"; - exit(1); - } - JNIEnv *env = nullptr; - jint result = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_8); - if (result != JNI_OK) { - LOG(FATAL) << "Failed to get JNI environment"; - exit(1); - } - jazzer::registerFuzzerCallbacks(*env); - jazzer::CoverageTracker::Setup(*env); - jazzer::SignalHandler::Setup(*env); - return JNI_VERSION_1_8; -} - namespace { constexpr auto kAgentBazelRunfilesPath = "jazzer/agent/jazzer_agent_deploy.jar"; constexpr auto kAgentFileName = "jazzer_agent_deploy.jar"; -constexpr const char kExceptionUtilsClassName[] = - "com/code_intelligence/jazzer/runtime/ExceptionUtils"; -} // namespace - -namespace jazzer { - -void DumpJvmStackTraces() { - JavaVM *vm; - jsize num_vms; - JNI_GetCreatedJavaVMs(&vm, 1, &num_vms); - if (num_vms != 1) { - return; - } - JNIEnv *env = nullptr; - if (vm->AttachCurrentThread(reinterpret_cast<void **>(&env), nullptr) != - JNI_OK) { - return; - } - jclass exceptionUtils = env->FindClass(kExceptionUtilsClassName); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return; - } - jmethodID dumpStack = - env->GetStaticMethodID(exceptionUtils, "dumpAllStackTraces", "()V"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return; - } - env->CallStaticVoidMethod(exceptionUtils, dumpStack); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return; - } - // Do not detach as we may be the main thread (but the JVM exits anyway). -} std::string dirFromFullPath(const std::string &path) { const auto pos = path.rfind(kPathSeparator); @@ -169,8 +157,8 @@ std::string getInstrumentorAgentPath(const std::string &executable_path) { // User provided agent location takes precedence. if (!FLAGS_agent_path.empty()) { if (std::ifstream(FLAGS_agent_path).good()) return FLAGS_agent_path; - LOG(ERROR) << "Could not find " << kAgentFileName << " at \"" - << FLAGS_agent_path << "\""; + std::cerr << "ERROR: Could not find " << kAgentFileName << " at \"" + << FLAGS_agent_path << "\"" << std::endl; exit(1); } // First check if we are running inside the Bazel tree and use the agent @@ -179,7 +167,7 @@ std::string getInstrumentorAgentPath(const std::string &executable_path) { using bazel::tools::cpp::runfiles::Runfiles; std::string error; std::unique_ptr<Runfiles> runfiles( - Runfiles::Create(executable_path, &error)); + Runfiles::Create(std::string(executable_path), &error)); if (runfiles != nullptr) { auto bazel_path = runfiles->Rlocation(kAgentBazelRunfilesPath); if (!bazel_path.empty() && std::ifstream(bazel_path).good()) @@ -193,31 +181,43 @@ std::string getInstrumentorAgentPath(const std::string &executable_path) { auto agent_path = absl::StrFormat("%s%c%s", dir, kPathSeparator, kAgentFileName); if (std::ifstream(agent_path).good()) return agent_path; - LOG(ERROR) << "Could not find " << kAgentFileName - << ". Please provide " - "the pathname via the --agent_path flag."; + std::cerr << "ERROR: Could not find " << kAgentFileName + << ". Please provide the pathname via the --agent_path flag." + << std::endl; exit(1); } -std::string agentArgsFromFlags() { - std::vector<std::string> args; - for (const auto &flag_pair : - std::vector<std::pair<std::string, const std::string &>>{ - // {<agent option>, <ref to glog flag> } - {"instrumentation_includes", FLAGS_instrumentation_includes}, - {"instrumentation_excludes", FLAGS_instrumentation_excludes}, - {"custom_hooks", FLAGS_custom_hooks}, - {"custom_hook_includes", FLAGS_custom_hook_includes}, - {"custom_hook_excludes", FLAGS_custom_hook_excludes}, - {"trace", FLAGS_trace}, - {"id_sync_file", FLAGS_id_sync_file}, - {"dump_classes_dir", FLAGS_dump_classes_dir}, - }) { - if (!flag_pair.second.empty()) { - args.push_back(flag_pair.first + "=" + flag_pair.second); - } +std::vector<std::string> optsAsDefines() { + std::vector<std::string> defines{ + absl::StrFormat("-Djazzer.target_class=%s", FLAGS_target_class), + absl::StrFormat("-Djazzer.target_args=%s", FLAGS_target_args), + absl::StrFormat("-Djazzer.dedup=%s", FLAGS_dedup ? "true" : "false"), + absl::StrFormat("-Djazzer.ignore=%s", FLAGS_ignore), + absl::StrFormat("-Djazzer.reproducer_path=%s", FLAGS_reproducer_path), + absl::StrFormat("-Djazzer.coverage_report=%s", FLAGS_coverage_report), + absl::StrFormat("-Djazzer.coverage_dump=%s", FLAGS_coverage_dump), + absl::StrFormat("-Djazzer.autofuzz=%s", FLAGS_autofuzz), + absl::StrFormat("-Djazzer.autofuzz_ignore=%s", FLAGS_autofuzz_ignore), + absl::StrFormat("-Djazzer.hooks=%s", FLAGS_hooks ? "true" : "false"), + absl::StrFormat("-Djazzer.id_sync_file=%s", FLAGS_id_sync_file), + absl::StrFormat("-Djazzer.instrumentation_includes=%s", + FLAGS_instrumentation_includes), + absl::StrFormat("-Djazzer.instrumentation_excludes=%s", + FLAGS_instrumentation_excludes), + absl::StrFormat("-Djazzer.custom_hooks=%s", FLAGS_custom_hooks), + absl::StrFormat("-Djazzer.disabled_hooks=%s", FLAGS_disabled_hooks), + absl::StrFormat("-Djazzer.custom_hook_includes=%s", + FLAGS_custom_hook_includes), + absl::StrFormat("-Djazzer.custom_hook_excludes=%s", + FLAGS_custom_hook_excludes), + absl::StrFormat("-Djazzer.trace=%s", FLAGS_trace), + absl::StrFormat("-Djazzer.dump_classes_dir=%s", FLAGS_dump_classes_dir), + }; + if (!gflags::GetCommandLineFlagInfoOrDie("keep_going").is_default) { + defines.emplace_back( + absl::StrFormat("-Djazzer.keep_going=%d", FLAGS_keep_going)); } - return absl::StrJoin(args, ","); + return defines; } // Splits a string at the ARG_SEPARATOR unless it is escaped with a backslash. @@ -247,6 +247,9 @@ std::vector<std::string> splitEscaped(const std::string &str) { return parts; } +} // namespace + +namespace jazzer { JVM::JVM(const std::string &executable_path) { // combine class path from command line flags and JAVA_FUZZER_CLASSPATH env @@ -254,11 +257,10 @@ JVM::JVM(const std::string &executable_path) { std::string class_path = absl::StrFormat("-Djava.class.path=%s", FLAGS_cp); const auto class_path_from_env = std::getenv("JAVA_FUZZER_CLASSPATH"); if (class_path_from_env) { - class_path += absl::StrFormat(ARG_SEPARATOR "%s", class_path_from_env); + class_path += absl::StrCat(ARG_SEPARATOR, class_path_from_env); } - class_path += absl::StrFormat(ARG_SEPARATOR "%s", - getInstrumentorAgentPath(executable_path)); - LOG(INFO) << "got class path " << class_path; + class_path += + absl::StrCat(ARG_SEPARATOR, getInstrumentorAgentPath(executable_path)); std::vector<JavaVMOption> options; options.push_back( @@ -266,13 +268,33 @@ JVM::JVM(const std::string &executable_path) { // Set the maximum heap size to a value that is slightly smaller than // libFuzzer's default rss_limit_mb. This prevents erroneous oom reports. options.push_back(JavaVMOption{.optionString = (char *)"-Xmx1800m"}); - options.push_back(JavaVMOption{.optionString = (char *)"-enableassertions"}); // Preserve and emit stack trace information even on hot paths. // This may hurt performance, but also helps find flaky bugs. options.push_back( JavaVMOption{.optionString = (char *)"-XX:-OmitStackTraceInFastThrow"}); // Optimize GC for high throughput rather than low latency. options.push_back(JavaVMOption{.optionString = (char *)"-XX:+UseParallelGC"}); + options.push_back( + JavaVMOption{.optionString = (char *)"-XX:+CriticalJNINatives"}); + + std::vector<std::string> opt_defines = optsAsDefines(); + for (const auto &define : opt_defines) { + options.push_back( + JavaVMOption{.optionString = const_cast<char *>(define.c_str())}); + } + + // Add additional JVM options set through JAVA_OPTS. + std::vector<std::string> java_opts_args; + const char *java_opts = std::getenv("JAVA_OPTS"); + if (java_opts != nullptr) { + // Mimic the behavior of the JVM when it sees JAVA_TOOL_OPTIONS. + std::cerr << "Picked up JAVA_OPTS: " << java_opts << std::endl; + java_opts_args = absl::StrSplit(java_opts, ' '); + for (const std::string &java_opt : java_opts_args) { + options.push_back( + JavaVMOption{.optionString = const_cast<char *>(java_opt.c_str())}); + } + } // add additional jvm options set through command line flags std::vector<std::string> jvm_args; @@ -292,15 +314,6 @@ JVM::JVM(const std::string &executable_path) { JavaVMOption{.optionString = const_cast<char *>(arg.c_str())}); } - std::string agent_jvm_arg; - if (FLAGS_hooks) { - agent_jvm_arg = absl::StrFormat("-javaagent:%s=%s", - getInstrumentorAgentPath(executable_path), - agentArgsFromFlags()); - options.push_back(JavaVMOption{ - .optionString = const_cast<char *>(agent_jvm_arg.c_str())}); - } - JavaVMInitArgs jvm_init_args = {.version = JNI_VERSION_1_8, .nOptions = (int)options.size(), .options = options.data(), @@ -316,167 +329,4 @@ JVM::JVM(const std::string &executable_path) { JNIEnv &JVM::GetEnv() const { return *env_; } JVM::~JVM() { jvm_->DestroyJavaVM(); } - -jclass JVM::FindClass(std::string class_name) const { - auto &env = GetEnv(); - std::replace(class_name.begin(), class_name.end(), '.', '/'); - const auto ret = env.FindClass(class_name.c_str()); - if (ret == nullptr) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error( - absl::StrFormat("Could not find class %s", class_name)); - } else { - throw std::runtime_error(absl::StrFormat( - "Java class '%s' not found without exception", class_name)); - } - } - return ret; -} - -jmethodID JVM::GetStaticMethodID(jclass jclass, const std::string &jmethod, - const std::string &signature, - bool is_required) const { - auto &env = GetEnv(); - const auto ret = - env.GetStaticMethodID(jclass, jmethod.c_str(), signature.c_str()); - if (ret == nullptr) { - if (is_required) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - } - throw std::runtime_error( - absl::StrFormat("Static method '%s' not found", jmethod)); - } else { - LOG(INFO) << "did not find method " << jmethod << " with signature " - << signature; - env.ExceptionClear(); - } - } - return ret; -} - -jmethodID JVM::GetMethodID(jclass jclass, const std::string &jmethod, - const std::string &signature) const { - auto &env = GetEnv(); - const auto ret = env.GetMethodID(jclass, jmethod.c_str(), signature.c_str()); - if (ret == nullptr) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - } - throw std::runtime_error(absl::StrFormat("Method '%s' not found", jmethod)); - } - return ret; -} - -jfieldID JVM::GetStaticFieldID(jclass class_id, const std::string &field_name, - const std::string &type) const { - auto &env = GetEnv(); - const auto ret = - env.GetStaticFieldID(class_id, field_name.c_str(), type.c_str()); - if (ret == nullptr) { - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - } - throw std::runtime_error( - absl::StrFormat("Field '%s' not found", field_name)); - } - return ret; -} - -ExceptionPrinter::ExceptionPrinter(JVM &jvm) - : jvm_(jvm), - string_writer_class_(jvm.FindClass("java/io/StringWriter")), - string_writer_constructor_( - jvm.GetMethodID(string_writer_class_, "<init>", "()V")), - string_writer_to_string_method_(jvm.GetMethodID( - string_writer_class_, "toString", "()Ljava/lang/String;")), - print_writer_class_(jvm.FindClass("java/io/PrintWriter")), - print_writer_constructor_(jvm.GetMethodID(print_writer_class_, "<init>", - "(Ljava/io/Writer;)V")) { - auto throwable_class = jvm.FindClass("java/lang/Throwable"); - print_stack_trace_method_ = jvm.GetMethodID( - throwable_class, "printStackTrace", "(Ljava/io/PrintWriter;)V"); - if (FLAGS_hooks) { - exception_utils_ = jvm.FindClass(kExceptionUtilsClassName); - compute_dedup_token_method_ = jvm.GetStaticMethodID( - exception_utils_, "computeDedupToken", "(Ljava/lang/Throwable;)J"); - preprocess_throwable_method_ = - jvm.GetStaticMethodID(exception_utils_, "preprocessThrowable", - "(Ljava/lang/Throwable;)Ljava/lang/Throwable;"); - } -} - -// The JNI way of writing: -// StringWriter stringWriter = new StringWriter(); -// PrintWriter printWriter = new PrintWriter(stringWriter); -// e.printStackTrace(printWriter); -// return stringWriter.toString(); -std::string ExceptionPrinter::getStackTrace(jthrowable exception) const { - auto &env = jvm_.GetEnv(); - if (exception == nullptr) { - return ""; - } - - auto string_writer = - env.NewObject(string_writer_class_, string_writer_constructor_); - if (string_writer == nullptr) { - env.ExceptionDescribe(); - return ""; - } - auto print_writer = env.NewObject(print_writer_class_, - print_writer_constructor_, string_writer); - if (print_writer == nullptr) { - env.ExceptionDescribe(); - return ""; - } - - env.CallVoidMethod(exception, print_stack_trace_method_, print_writer); - env.DeleteLocalRef(print_writer); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - return ""; - } - auto exception_string_object = reinterpret_cast<jstring>( - env.CallObjectMethod(string_writer, string_writer_to_string_method_)); - env.DeleteLocalRef(string_writer); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - return ""; - } - - auto char_pointer = env.GetStringUTFChars(exception_string_object, nullptr); - std::string exception_string(char_pointer); - env.ReleaseStringUTFChars(exception_string_object, char_pointer); - env.DeleteLocalRef(exception_string_object); - return exception_string; -} - -jthrowable ExceptionPrinter::preprocessException(jthrowable exception) const { - if (exception == nullptr) return nullptr; - auto &env = jvm_.GetEnv(); - if (!FLAGS_hooks || !preprocess_throwable_method_) return exception; - auto processed_exception = (jthrowable)(env.CallStaticObjectMethod( - exception_utils_, preprocess_throwable_method_, exception)); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - return exception; - } - return processed_exception; -} - -jlong ExceptionPrinter::computeDedupToken(jthrowable exception) const { - auto &env = jvm_.GetEnv(); - if (!FLAGS_hooks || exception == nullptr || - compute_dedup_token_method_ == nullptr) - return 0; - const auto dedup_token = env.CallStaticLongMethod( - exception_utils_, compute_dedup_token_method_, exception); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - return 0; - } - return dedup_token; -} - } // namespace jazzer diff --git a/driver/jvm_tooling.h b/driver/jvm_tooling.h index be9582de..2a4a133c 100644 --- a/driver/jvm_tooling.h +++ b/driver/jvm_tooling.h @@ -42,46 +42,5 @@ class JVM { // Get the JNI environment for interaction with the running JVM instance. JNIEnv &GetEnv() const; - - jclass FindClass(std::string class_name) const; - jmethodID GetStaticMethodID(jclass class_id, const std::string &method_name, - const std::string &signature, - bool is_required = true) const; - jmethodID GetMethodID(jclass class_id, const std::string &method_name, - const std::string &signature) const; - jfieldID GetStaticFieldID(jclass jclass, const std::string &field_name, - const std::string &type) const; -}; - -// Adds convenience methods to convert a jvm exception to std::string -// using StringWriter and PrintWriter. The stack trace can be subjected to -// further processing, such as deduplication token computation and severity -// annotation. -class ExceptionPrinter { - private: - const JVM &jvm_; - - jclass string_writer_class_; - jmethodID string_writer_constructor_; - jmethodID string_writer_to_string_method_; - - jclass print_writer_class_; - jmethodID print_writer_constructor_; - jmethodID print_stack_trace_method_; - - jclass exception_utils_; - jmethodID compute_dedup_token_method_; - jmethodID preprocess_throwable_method_; - - protected: - explicit ExceptionPrinter(JVM &jvm); - - // returns the current JVM exception stack trace as a string - std::string getStackTrace(jthrowable exception) const; - // augments the throwable with additional information such as severity markers - jthrowable preprocessException(jthrowable exception) const; - // returns a hash of the exception stack trace for deduplication purposes - jlong computeDedupToken(jthrowable exception) const; }; - } /* namespace jazzer */ diff --git a/driver/jvm_tooling_test.cpp b/driver/jvm_tooling_test.cpp index f2e8c66a..5aceadd4 100644 --- a/driver/jvm_tooling_test.cpp +++ b/driver/jvm_tooling_test.cpp @@ -14,18 +14,15 @@ #include "jvm_tooling.h" -#include "coverage_tracker.h" -#include "fuzz_target_runner.h" +#include <memory> + #include "gflags/gflags.h" #include "gtest/gtest.h" #include "tools/cpp/runfiles/runfiles.h" DECLARE_string(cp); +DECLARE_bool(hooks); DECLARE_string(jvm_args); -DECLARE_string(target_class); -DECLARE_string(target_args); -DECLARE_string(agent_path); -DECLARE_string(instrumentation_excludes); #ifdef _WIN32 #define ARG_SEPARATOR ";" @@ -35,27 +32,20 @@ DECLARE_string(instrumentation_excludes); namespace jazzer { -std::vector<std::string> splitOnSpace(const std::string &s); - -TEST(SpaceSplit, SpaceSplitSimple) { - ASSERT_EQ((std::vector<std::string>{"first", "se\\ cond", "third"}), - splitOnSpace("first se\\ cond third")); -} - class JvmToolingTest : public ::testing::Test { protected: // After DestroyJavaVM() no new JVM instance can be created in the same // process, so we set up a single JVM instance for this test binary which gets // destroyed after all tests in this test suite have finished. static void SetUpTestCase() { + FLAGS_hooks = false; FLAGS_jvm_args = "-Denv1=va\\" ARG_SEPARATOR "l1\\\\" ARG_SEPARATOR "-Denv2=val2"; - FLAGS_instrumentation_excludes = "**"; using ::bazel::tools::cpp::runfiles::Runfiles; Runfiles *runfiles = Runfiles::CreateForTest(); FLAGS_cp = runfiles->Rlocation(FLAGS_cp); - jvm_ = std::make_unique<JVM>("test_executable"); + jvm_ = std::unique_ptr<JVM>(new JVM("test_executable")); } static void TearDownTestCase() { jvm_.reset(nullptr); } @@ -65,26 +55,15 @@ class JvmToolingTest : public ::testing::Test { std::unique_ptr<JVM> JvmToolingTest::jvm_ = nullptr; -TEST_F(JvmToolingTest, ClassNotFound) { - ASSERT_THROW(jvm_->FindClass(""), std::runtime_error); - ASSERT_THROW(jvm_->FindClass("test.NonExistingClass"), std::runtime_error); - ASSERT_THROW(jvm_->FindClass("test/NonExistingClass"), std::runtime_error); -} - -TEST_F(JvmToolingTest, ClassInClassPath) { - ASSERT_NE(nullptr, jvm_->FindClass("test.PropertyPrinter")); - ASSERT_NE(nullptr, jvm_->FindClass("test/PropertyPrinter")); -} - TEST_F(JvmToolingTest, JniProperties) { - auto property_printer_class = jvm_->FindClass("test.PropertyPrinter"); + auto &env = jvm_->GetEnv(); + auto property_printer_class = env.FindClass("test/PropertyPrinter"); ASSERT_NE(nullptr, property_printer_class); auto method_id = - jvm_->GetStaticMethodID(property_printer_class, "printProperty", - "(Ljava/lang/String;)Ljava/lang/String;"); + env.GetStaticMethodID(property_printer_class, "printProperty", + "(Ljava/lang/String;)Ljava/lang/String;"); ASSERT_NE(nullptr, method_id); - auto &env = jvm_->GetEnv(); for (const auto &el : std::vector<std::pair<std::string, std::string>>{ {"not set property", ""}, {"env1", "va" ARG_SEPARATOR "l1\\"}, @@ -98,102 +77,8 @@ TEST_F(JvmToolingTest, JniProperties) { } else { ASSERT_NE(nullptr, ret); jboolean is_copy; - ASSERT_EQ(el.second, jvm_->GetEnv().GetStringUTFChars(ret, &is_copy)); + ASSERT_EQ(el.second, env.GetStringUTFChars(ret, &is_copy)); } } } - -TEST_F(JvmToolingTest, SimpleFuzzTarget) { - // see testdata/test/SimpleFuzzTarget.java for the implementation of the fuzz - // target - FLAGS_target_class = "test/SimpleFuzzTarget"; - FLAGS_target_args = ""; - FuzzTargetRunner fuzz_target_runner(*jvm_); - - // normal case: fuzzerTestOneInput returns false - std::string input("random"); - ASSERT_EQ(RunResult::kOk, fuzz_target_runner.Run( - (const uint8_t *)input.c_str(), input.size())); - - // exception is thrown in fuzzerTestOneInput - input = "crash"; - ASSERT_EQ( - RunResult::kException, - fuzz_target_runner.Run((const uint8_t *)input.c_str(), input.size())); -} - -class ExceptionPrinterTest : public ExceptionPrinter { - public: - ExceptionPrinterTest(JVM &jvm) : ExceptionPrinter(jvm), jvm_(jvm) {} - - std::string TriggerJvmException() { - jclass illegal_argument_exception = - jvm_.FindClass("java.lang.IllegalArgumentException"); - jvm_.GetEnv().ThrowNew(illegal_argument_exception, "Test"); - jthrowable exception = jvm_.GetEnv().ExceptionOccurred(); - jvm_.GetEnv().ExceptionClear(); - return getStackTrace(exception); - } - - private: - const JVM &jvm_; -}; - -TEST_F(JvmToolingTest, ExceptionPrinter) { - ExceptionPrinterTest exception_printer(*jvm_); - // a.k.a std::string.startsWith(java.lang...) - ASSERT_TRUE(exception_printer.TriggerJvmException().rfind( - "java.lang.IllegalArgumentException", 0) == 0); -} - -TEST_F(JvmToolingTest, FuzzTargetWithInit) { - // see testdata/test/FuzzTargetWithInit.java for the implementation of the - // fuzz target. All string arguments provided in fuzzerInitialize(String[]) - // will cause a crash if input in fuzzerTestOneInput(byte[]). - FLAGS_target_class = "test/FuzzTargetWithInit"; - FLAGS_target_args = "crash_now crash_harder"; - FuzzTargetRunner fuzz_target_runner(*jvm_); - - // normal case: fuzzerTestOneInput returns false - std::string input("random"); - ASSERT_EQ(RunResult::kOk, fuzz_target_runner.Run( - (const uint8_t *)input.c_str(), input.size())); - - input = "crash_now"; - ASSERT_EQ( - RunResult::kException, - fuzz_target_runner.Run((const uint8_t *)input.c_str(), input.size())); - - input = "this is harmless"; - ASSERT_EQ(RunResult::kOk, fuzz_target_runner.Run( - (const uint8_t *)input.c_str(), input.size())); - - input = "crash_harder"; - ASSERT_EQ( - RunResult::kException, - fuzz_target_runner.Run((const uint8_t *)input.c_str(), input.size())); -} - -TEST_F(JvmToolingTest, TestCoverageMap) { - CoverageTracker::Clear(); - // check that after the initial clear the first coverage counter is 0 - auto coverage_counters_array = CoverageTracker::GetCoverageCounters(); - ASSERT_EQ(0, coverage_counters_array[0]); - - FLAGS_target_class = "test/FuzzTargetWithCoverage"; - FLAGS_target_args = ""; - FuzzTargetRunner fuzz_target_runner(*jvm_); - // run a fuzz target input which will cause the first coverage counter to - // increase - fuzz_target_runner.Run(nullptr, 0); - ASSERT_EQ(1, coverage_counters_array[0]); - CoverageTracker::Clear(); - // back to initial state - ASSERT_EQ(0, coverage_counters_array[0]); - - // calling the fuzz target twice - fuzz_target_runner.Run(nullptr, 0); - fuzz_target_runner.Run(nullptr, 0); - ASSERT_EQ(2, coverage_counters_array[0]); -} } // namespace jazzer diff --git a/driver/libfuzzer_callbacks.cpp b/driver/libfuzzer_callbacks.cpp deleted file mode 100644 index 5b7813dd..00000000 --- a/driver/libfuzzer_callbacks.cpp +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "libfuzzer_callbacks.h" - -#include <jni.h> - -#include <fstream> -#include <iostream> -#include <mutex> -#include <utility> -#include <vector> - -#include "absl/strings/match.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_split.h" -#include "gflags/gflags.h" -#include "glog/logging.h" -#include "sanitizer_hooks_with_pc.h" - -DEFINE_bool( - fake_pcs, false, - "Supply synthetic Java program counters to libFuzzer trace hooks to " - "make value profiling more effective. Enabled by default if " - "-use_value_profile=1 is specified."); - -namespace { - -const char kLibfuzzerTraceDataFlowHooksClass[] = - "com/code_intelligence/jazzer/runtime/" - "TraceDataFlowNativeCallbacks"; - -extern "C" { -void __sanitizer_weak_hook_memcmp(void *caller_pc, const void *s1, - const void *s2, std::size_t n, int result); -void __sanitizer_weak_hook_compare_bytes(void *caller_pc, const void *s1, - const void *s2, std::size_t n1, - std::size_t n2, int result); -void __sanitizer_weak_hook_strcmp(void *caller_pc, const char *s1, - const char *s2, int result); -void __sanitizer_weak_hook_strstr(void *caller_pc, const char *s1, - const char *s2, const char *result); -void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2); -void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2); - -void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases); - -void __sanitizer_cov_trace_div4(uint32_t val); -void __sanitizer_cov_trace_div8(uint64_t val); - -void __sanitizer_cov_trace_gep(uintptr_t idx); -} - -inline __attribute__((always_inline)) void *idToPc(jint id) { - return reinterpret_cast<void *>(static_cast<uintptr_t>(id)); -} - -void JNICALL libfuzzerStringCompareCallback(JNIEnv &env, jclass cls, jstring s1, - jstring s2, jint result, jint id) { - const char *s1_native = env.GetStringUTFChars(s1, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - std::size_t n1 = env.GetStringUTFLength(s1); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - const char *s2_native = env.GetStringUTFChars(s2, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - std::size_t n2 = env.GetStringUTFLength(s2); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - __sanitizer_weak_hook_compare_bytes(idToPc(id), s1_native, s2_native, n1, n2, - result); - env.ReleaseStringUTFChars(s1, s1_native); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - env.ReleaseStringUTFChars(s2, s2_native); - if (env.ExceptionCheck()) env.ExceptionDescribe(); -} - -void JNICALL libfuzzerStringContainCallback(JNIEnv &env, jclass cls, jstring s1, - jstring s2, jint id) { - const char *s1_native = env.GetStringUTFChars(s1, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - const char *s2_native = env.GetStringUTFChars(s2, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - // libFuzzer currently ignores the result, which allows us to simply pass a - // valid but arbitrary pointer here instead of performing an actual strstr - // operation. - __sanitizer_weak_hook_strstr(idToPc(id), s1_native, s2_native, s1_native); - env.ReleaseStringUTFChars(s1, s1_native); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - env.ReleaseStringUTFChars(s2, s2_native); - if (env.ExceptionCheck()) env.ExceptionDescribe(); -} - -void JNICALL libfuzzerByteCompareCallback(JNIEnv &env, jclass cls, - jbyteArray b1, jbyteArray b2, - jint result, jint id) { - jbyte *b1_native = env.GetByteArrayElements(b1, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - jbyte *b2_native = env.GetByteArrayElements(b2, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - jint b1_length = env.GetArrayLength(b1); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - jint b2_length = env.GetArrayLength(b2); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, - b1_length, b2_length, result); - env.ReleaseByteArrayElements(b1, b1_native, JNI_ABORT); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - env.ReleaseByteArrayElements(b2, b2_native, JNI_ABORT); - if (env.ExceptionCheck()) env.ExceptionDescribe(); -} - -void JNICALL libfuzzerLongCompareCallback(JNIEnv &env, jclass cls, jlong value1, - jlong value2, jint id) { - __sanitizer_cov_trace_cmp8(value1, value2); -} - -void JNICALL libfuzzerLongCompareCallbackWithPc(JNIEnv &env, jclass cls, - jlong value1, jlong value2, - jint id) { - __sanitizer_cov_trace_cmp8_with_pc(idToPc(id), value1, value2); -} - -void JNICALL libfuzzerIntCompareCallback(JNIEnv &env, jclass cls, jint value1, - jint value2, jint id) { - __sanitizer_cov_trace_cmp4(value1, value2); -} - -void JNICALL libfuzzerIntCompareCallbackWithPc(JNIEnv &env, jclass cls, - jint value1, jint value2, - jint id) { - __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); -} - -void JNICALL libfuzzerSwitchCaseCallback(JNIEnv &env, jclass cls, - jlong switch_value, - jlongArray libfuzzer_case_values, - jint id) { - jlong *case_values = env.GetLongArrayElements(libfuzzer_case_values, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - __sanitizer_cov_trace_switch(switch_value, - reinterpret_cast<uint64_t *>(case_values)); - env.ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); - if (env.ExceptionCheck()) env.ExceptionDescribe(); -} - -void JNICALL libfuzzerSwitchCaseCallbackWithPc(JNIEnv &env, jclass cls, - jlong switch_value, - jlongArray libfuzzer_case_values, - jint id) { - jlong *case_values = env.GetLongArrayElements(libfuzzer_case_values, nullptr); - if (env.ExceptionCheck()) env.ExceptionDescribe(); - __sanitizer_cov_trace_switch_with_pc( - idToPc(id), switch_value, reinterpret_cast<uint64_t *>(case_values)); - env.ReleaseLongArrayElements(libfuzzer_case_values, case_values, JNI_ABORT); - if (env.ExceptionCheck()) env.ExceptionDescribe(); -} - -void JNICALL libfuzzerLongDivCallback(JNIEnv &env, jclass cls, jlong value, - jint id) { - __sanitizer_cov_trace_div8(value); -} - -void JNICALL libfuzzerLongDivCallbackWithPc(JNIEnv &env, jclass cls, - jlong value, jint id) { - __sanitizer_cov_trace_div8_with_pc(idToPc(id), value); -} - -void JNICALL libfuzzerIntDivCallback(JNIEnv &env, jclass cls, jint value, - jint id) { - __sanitizer_cov_trace_div4(value); -} - -void JNICALL libfuzzerIntDivCallbackWithPc(JNIEnv &env, jclass cls, jint value, - jint id) { - __sanitizer_cov_trace_div4_with_pc(idToPc(id), value); -} - -void JNICALL libfuzzerGepCallback(JNIEnv &env, jclass cls, jlong idx, jint id) { - __sanitizer_cov_trace_gep(static_cast<uintptr_t>(idx)); -} - -void JNICALL libfuzzerGepCallbackWithPc(JNIEnv &env, jclass cls, jlong idx, - jint id) { - __sanitizer_cov_trace_gep_with_pc(idToPc(id), static_cast<uintptr_t>(idx)); -} - -void JNICALL libfuzzerPcIndirCallback(JNIEnv &env, jclass cls, jint caller_id, - jint callee_id) { - __sanitizer_cov_trace_pc_indir_with_pc(idToPc(caller_id), - static_cast<uintptr_t>(callee_id)); -} - -bool is_using_native_libraries = false; -std::once_flag ignore_list_flag; -std::vector<std::pair<uintptr_t, uintptr_t>> ignore_for_interception_ranges; - -extern "C" [[maybe_unused]] bool __sanitizer_weak_is_relevant_pc( - void *caller_pc) { - // If the fuzz target is not using native libraries, calls to strcmp, memcmp, - // etc. should never be intercepted. The values reported if they were at best - // duplicate the values received from our bytecode instrumentation and at - // worst pollute the table of recent compares with string internal to the JDK. - if (!is_using_native_libraries) return false; - // If the fuzz target is using native libraries, intercept calls only if they - // don't originate from those address ranges that are known to belong to the - // JDK. - return std::none_of(ignore_for_interception_ranges.cbegin(), - ignore_for_interception_ranges.cend(), - [caller_pc](const auto &range) { - uintptr_t start; - uintptr_t end; - std::tie(start, end) = range; - auto address = reinterpret_cast<uintptr_t>(caller_pc); - return start <= address && address <= end; - }); -} - -/** - * Adds the address ranges of executable segmentes of the library lib_name to - * the ignorelist for C standard library function interception (strcmp, memcmp, - * ...). - */ -void ignoreLibraryForInterception(const std::string &lib_name) { - const auto num_address_ranges = ignore_for_interception_ranges.size(); - std::ifstream loaded_libs("/proc/self/maps"); - if (!loaded_libs) { - // This early exit is taken e.g. on macOS, where /proc does not exist. - return; - } - std::string line; - while (std::getline(loaded_libs, line)) { - if (!absl::StrContains(line, lib_name)) continue; - // clang-format off - // A typical line looks as follows: - // 7f15356c9000-7f1536367000 r-xp 0020d000 fd:01 19275673 /usr/lib/jvm/java-15-openjdk-amd64/lib/server/libjvm.so - // clang-format on - std::vector<std::string_view> parts = - absl::StrSplit(line, ' ', absl::SkipEmpty()); - if (parts.size() != 6) { - std::cout << "ERROR: Invalid format for /proc/self/maps\n" - << line << std::endl; - exit(1); - } - // Skip non-executable address rang"s. - if (!absl::StrContains(parts[1], "x")) continue; - std::string_view range_str = parts[0]; - std::vector<std::string> range = absl::StrSplit(range_str, "-"); - if (range.size() != 2) { - std::cout - << "ERROR: Unexpected address range format in /proc/self/maps line: " - << range_str << std::endl; - exit(1); - } - std::size_t pos; - auto start = std::stoull(range[0], &pos, 16); - if (pos != range[0].size()) { - std::cout - << "ERROR: Unexpected address range format in /proc/self/maps line: " - << range_str << std::endl; - exit(1); - } - auto end = std::stoull(range[1], &pos, 16); - if (pos != range[0].size()) { - std::cout - << "ERROR: Unexpected address range format in /proc/self/maps line: " - << range_str << std::endl; - exit(1); - } - ignore_for_interception_ranges.emplace_back(start, end); - } - const auto num_code_segments = - ignore_for_interception_ranges.size() - num_address_ranges; - LOG(INFO) << "added " << num_code_segments - << " code segment of native library " << lib_name - << " to interceptor ignorelist"; -} - -const std::vector<std::string> kLibrariesToIgnoreForInterception = { - // The driver executable itself can be treated just like a library. - "jazzer_driver", "libinstrument.so", "libjava.so", - "libjimage.so", "libjli.so", "libjvm.so", - "libnet.so", "libverify.so", "libzip.so", -}; - -void JNICALL handleLibraryLoad(JNIEnv &env, jclass cls) { - std::call_once(ignore_list_flag, [] { - LOG(INFO) - << "detected a native library load, enabling interception for libc " - "functions"; - for (const auto &lib_name : kLibrariesToIgnoreForInterception) - ignoreLibraryForInterception(lib_name); - // Enable the ignore list after it has been populated since vector is not - // thread-safe with respect to concurrent writes and reads. - is_using_native_libraries = true; - }); -} - -void registerCallback(JNIEnv &env, const char *java_hooks_class_name, - const JNINativeMethod *methods, int num_methods) { - auto java_hooks_class = env.FindClass(java_hooks_class_name); - if (java_hooks_class == nullptr) { - env.ExceptionDescribe(); - throw std::runtime_error( - absl::StrFormat("could not find class %s", java_hooks_class_name)); - } - LOG(INFO) << "registering hooks for class " << java_hooks_class_name; - env.RegisterNatives(java_hooks_class, methods, num_methods); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error("could not register native callbacks"); - } -} -} // namespace - -namespace jazzer { - -bool registerFuzzerCallbacks(JNIEnv &env) { - if (FLAGS_fake_pcs) { - LOG(INFO) << "using callback variants with fake pcs"; - CalibrateTrampoline(); - } - { - JNINativeMethod string_methods[]{ - {(char *)"traceMemcmp", (char *)"([B[BII)V", - (void *)&libfuzzerByteCompareCallback}, - {(char *)"traceStrcmp", - (char *)"(Ljava/lang/String;Ljava/lang/String;II)V", - (void *)&libfuzzerStringCompareCallback}, - {(char *)"traceStrstr", - (char *)"(Ljava/lang/String;Ljava/lang/String;I)V", - (void *)&libfuzzerStringContainCallback}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, string_methods, - sizeof(string_methods) / sizeof(string_methods[0])); - } - - { - JNINativeMethod cmp_methods[]{ - {(char *)"traceCmpLong", (char *)"(JJI)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerLongCompareCallbackWithPc - : &libfuzzerLongCompareCallback)}, - {(char *)"traceCmpInt", (char *)"(III)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerIntCompareCallbackWithPc - : &libfuzzerIntCompareCallback)}, - // libFuzzer internally treats const comparisons the same as - // non-constant cmps. - {(char *)"traceConstCmpInt", (char *)"(III)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerIntCompareCallbackWithPc - : &libfuzzerIntCompareCallback)}, - {(char *)"traceSwitch", (char *)"(J[JI)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerSwitchCaseCallbackWithPc - : &libfuzzerSwitchCaseCallback)}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, cmp_methods, - sizeof(cmp_methods) / sizeof(cmp_methods[0])); - } - - { - JNINativeMethod div_methods[]{ - {(char *)"traceDivLong", (char *)"(JI)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerLongDivCallbackWithPc - : &libfuzzerLongDivCallback)}, - {(char *)"traceDivInt", (char *)"(II)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerIntDivCallbackWithPc - : &libfuzzerIntDivCallback)}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, div_methods, - sizeof(div_methods) / sizeof(div_methods[0])); - } - - { - JNINativeMethod gep_methods[]{ - {(char *)"traceGep", (char *)"(JI)V", - (void *)(FLAGS_fake_pcs ? &libfuzzerGepCallbackWithPc - : &libfuzzerGepCallback)}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, gep_methods, - sizeof(gep_methods) / sizeof(gep_methods[0])); - } - - { - JNINativeMethod indir_methods[]{{(char *)"tracePcIndir", (char *)"(II)V", - (void *)(&libfuzzerPcIndirCallback)}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, indir_methods, - sizeof(indir_methods) / sizeof(indir_methods[0])); - } - - { - JNINativeMethod native_methods[]{{(char *)"handleLibraryLoad", - (char *)"()V", - (void *)(&handleLibraryLoad)}}; - - registerCallback(env, kLibfuzzerTraceDataFlowHooksClass, native_methods, - sizeof(native_methods) / sizeof(native_methods[0])); - } - - return env.ExceptionCheck(); -} - -} // namespace jazzer diff --git a/driver/libfuzzer_driver.cpp b/driver/libfuzzer_driver.cpp deleted file mode 100644 index 57beef58..00000000 --- a/driver/libfuzzer_driver.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "libfuzzer_driver.h" - -#include <rules_jni.h> - -#include <algorithm> -#include <filesystem> -#include <fstream> -#include <random> -#include <string> -#include <vector> - -#include "absl/strings/match.h" -#include "absl/strings/str_format.h" -#include "fuzz_target_runner.h" -#include "gflags/gflags.h" -#include "glog/logging.h" -#include "jvm_tooling.h" - -using namespace std::string_literals; - -// Defined by glog -DECLARE_bool(log_prefix); - -// Defined in libfuzzer_callbacks.cpp -DECLARE_bool(fake_pcs); - -// Defined in jvm_tooling.cpp -DECLARE_string(id_sync_file); - -// Defined in fuzz_target_runner.cpp -DECLARE_string(coverage_report); - -// This symbol is defined by sanitizers if linked into Jazzer or in -// sanitizer_symbols.cpp if no sanitizer is used. -extern "C" void __sanitizer_set_death_callback(void (*)()); - -// We apply a patch to libFuzzer to make it call this function instead of -// __sanitizer_set_death_callback to pass us the death callback. -extern "C" [[maybe_unused]] void __jazzer_set_death_callback( - void (*callback)()) { - jazzer::AbstractLibfuzzerDriver::libfuzzer_print_crashing_input_ = callback; - __sanitizer_set_death_callback(callback); -} - -namespace { -char *additional_arg; -std::vector<char *> modified_argv; - -std::string GetNewTempFilePath() { - auto temp_dir = std::filesystem::temp_directory_path(); - - std::string temp_filename_suffix(32, '\0'); - std::random_device rng; - std::uniform_int_distribution<short> dist(0, 'z' - 'a'); - std::generate_n(temp_filename_suffix.begin(), temp_filename_suffix.length(), - [&rng, &dist] { return static_cast<char>('a' + dist(rng)); }); - - auto temp_path = temp_dir / ("jazzer-" + temp_filename_suffix); - if (std::filesystem::exists(temp_path)) - throw std::runtime_error("Random temp file path exists: " + - temp_path.string()); - return temp_path.string(); -} -} // namespace - -namespace jazzer { -// A libFuzzer-registered callback that outputs the crashing input, but does -// not include a stack trace. -void (*AbstractLibfuzzerDriver::libfuzzer_print_crashing_input_)() = nullptr; - -AbstractLibfuzzerDriver::AbstractLibfuzzerDriver( - int *argc, char ***argv, const std::string &usage_string) { - gflags::SetUsageMessage(usage_string); - // Disable glog log prefixes to mimic libFuzzer output. - FLAGS_log_prefix = false; - google::InitGoogleLogging((*argv)[0]); - rules_jni_init((*argv)[0]); - - auto argv_start = *argv; - auto argv_end = *argv + *argc; - - if (std::find(argv_start, argv_end, "-use_value_profile=1"s) != argv_end) { - FLAGS_fake_pcs = true; - } - - // All libFuzzer flags start with a single dash, our arguments all start with - // a double dash. We can thus filter out the arguments meant for gflags by - // taking only those with a leading double dash. - std::vector<char *> our_args = {*argv_start}; - std::copy_if( - argv_start, argv_end, std::back_inserter(our_args), - [](const auto arg) { return absl::StartsWith(std::string(arg), "--"); }); - int our_argc = our_args.size(); - char **our_argv = our_args.data(); - // Let gflags consume its flags, but keep them in the argument list in case - // libFuzzer forwards the command line (e.g. with -jobs or -minimize_crash). - gflags::ParseCommandLineFlags(&our_argc, &our_argv, false); - - if (std::any_of(argv_start, argv_end, [](const std::string_view &arg) { - return absl::StartsWith(arg, "-fork=") || - absl::StartsWith(arg, "-jobs=") || - absl::StartsWith(arg, "-merge="); - })) { - if (!FLAGS_coverage_report.empty()) { - LOG(WARNING) << "WARN: --coverage_report does not support parallel " - "fuzzing and has been disabled"; - FLAGS_coverage_report = ""; - } - if (FLAGS_id_sync_file.empty()) { - // Create an empty temporary file used for coverage ID synchronization and - // pass its path to the agent in every child process. This requires adding - // the argument to argv for it to be picked up by libFuzzer, which then - // forwards it to child processes. - FLAGS_id_sync_file = GetNewTempFilePath(); - std::string new_arg = - absl::StrFormat("--id_sync_file=%s", FLAGS_id_sync_file); - // This argument can be accessed by libFuzzer at any (later) time and thus - // cannot be safely freed by us. - additional_arg = strdup(new_arg.c_str()); - modified_argv = std::vector<char *>(argv_start, argv_end); - modified_argv.push_back(additional_arg); - // Terminate modified_argv. - modified_argv.push_back(nullptr); - // Modify argv and argc for libFuzzer. modified_argv must not be changed - // after this point. - *argc += 1; - *argv = modified_argv.data(); - argv_start = *argv; - argv_end = *argv + *argc; - } - // Creates the file, truncating it if it exists. - std::ofstream touch_file(FLAGS_id_sync_file, std::ios_base::trunc); - - auto cleanup_fn = [] { - try { - std::filesystem::remove(std::filesystem::path(FLAGS_id_sync_file)); - } catch (...) { - // We should not throw exceptions during shutdown. - } - }; - std::atexit(cleanup_fn); - } - - initJvm(*argv_start); -} - -void AbstractLibfuzzerDriver::initJvm(const std::string &executable_path) { - jvm_ = std::make_unique<jazzer::JVM>(executable_path); -} - -LibfuzzerDriver::LibfuzzerDriver(int *argc, char ***argv) - : AbstractLibfuzzerDriver(argc, argv, getUsageString()) { - // the FuzzTargetRunner can only be initialized after the fuzzer callbacks - // have been registered otherwise link errors would occur - runner_ = std::make_unique<jazzer::FuzzTargetRunner>(*jvm_); -} - -std::string LibfuzzerDriver::getUsageString() { - return R"(Test java fuzz targets using libFuzzer. Usage: - jazzer --cp=<java_class_path> --target_class=<fuzz_target_class> <libfuzzer_arguments...>)"; -} - -RunResult LibfuzzerDriver::TestOneInput(const uint8_t *data, - const std::size_t size) { - // pass the fuzzer input to the java fuzz target - return runner_->Run(data, size); -} - -void LibfuzzerDriver::DumpReproducer(const uint8_t *data, std::size_t size) { - return runner_->DumpReproducer(data, size); -} - -} // namespace jazzer diff --git a/driver/libfuzzer_driver.h b/driver/libfuzzer_driver.h deleted file mode 100644 index 557277a5..00000000 --- a/driver/libfuzzer_driver.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2021 Code Intelligence GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <memory> -#include <string> - -#include "absl/strings/match.h" -#include "fuzz_target_runner.h" -#include "fuzzed_data_provider.h" -#include "jvm_tooling.h" -#include "libfuzzer_callbacks.h" -#include "signal_handler.h" - -namespace jazzer { - -class AbstractLibfuzzerDriver { - public: - AbstractLibfuzzerDriver(int *argc, char ***argv, - const std::string &usage_string); - - virtual ~AbstractLibfuzzerDriver() = default; - - virtual RunResult TestOneInput(const uint8_t *data, std::size_t size) = 0; - - // Default value of the libFuzzer -error_exitcode flag. - static constexpr int kErrorExitCode = 77; - - // A libFuzzer-registered callback that outputs the crashing input, but does - // not include a stack trace. - static void (*libfuzzer_print_crashing_input_)(); - - protected: - // wrapper around the running jvm instance - std::unique_ptr<jazzer::JVM> jvm_; - - private: - // forwards signals caught while the JVM is running - std::unique_ptr<jazzer::SignalHandler> signal_handler_; - - void initJvm(const std::string &executable_path); -}; - -class LibfuzzerDriver : public AbstractLibfuzzerDriver { - public: - LibfuzzerDriver(int *argc, char ***argv); - - RunResult TestOneInput(const uint8_t *data, std::size_t size) override; - - ~LibfuzzerDriver() override = default; - - void DumpReproducer(const uint8_t *data, std::size_t size); - - private: - // initializes the fuzz target and invokes the TestOneInput function - std::unique_ptr<jazzer::FuzzTargetRunner> runner_; - - static std::string getUsageString(); -}; - -} // namespace jazzer diff --git a/driver/libfuzzer_fuzz_target.cpp b/driver/libfuzzer_fuzz_target.cpp deleted file mode 100644 index d258e519..00000000 --- a/driver/libfuzzer_fuzz_target.cpp +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include <iostream> - -#include "libfuzzer_driver.h" - -namespace { -bool is_asan_active = false; -} - -extern "C" { -const char *__asan_default_options() { - is_asan_active = true; - // LeakSanitizer is not yet supported as it reports too many false positives - // due to how the JVM GC works. - // We use a distinguished exit code to recognize ASan crashes in tests. - // Also specify abort_on_error=0 explicitly since ASan aborts rather than - // exits on macOS by default, which would cause our exit code to be ignored. - return "abort_on_error=0,detect_leaks=0,exitcode=76"; -} - -const char *__ubsan_default_options() { - // We use a distinguished exit code to recognize UBSan crashes in tests. - // Also specify abort_on_error=0 explicitly since UBSan aborts rather than - // exits on macOS by default, which would cause our exit code to be ignored. - return "abort_on_error=0,exitcode=76"; -} -} - -namespace { -using Driver = jazzer::LibfuzzerDriver; - -std::unique_ptr<Driver> gLibfuzzerDriver; -} // namespace - -extern "C" void driver_cleanup() { - // Free the libfuzzer driver which triggers a clean JVM shutdown. - gLibfuzzerDriver.reset(nullptr); -} - -// Entry point called by libfuzzer before any LLVMFuzzerTestOneInput(...) -// invocations. -extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) { - if (is_asan_active) { - std::cerr << "WARN: Jazzer is not compatible with LeakSanitizer yet. Leaks " - "are not reported." - << std::endl; - } - gLibfuzzerDriver = std::make_unique<Driver>(argc, argv); - std::atexit(&driver_cleanup); - return 0; -} - -// Called by the fuzzer for every fuzzing input. -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, const size_t size) { - auto result = gLibfuzzerDriver->TestOneInput(data, size); - if (result != jazzer::RunResult::kOk) { - // Fuzzer triggered an exception or assertion in Java code. Skip the - // uninformative libFuzzer stack trace. - std::cerr << "== libFuzzer crashing input ==\n"; - Driver::libfuzzer_print_crashing_input_(); - // DumpReproducer needs to be called after libFuzzer printed its final - // stats as otherwise it would report incorrect coverage. - gLibfuzzerDriver->DumpReproducer(data, size); - if (result == jazzer::RunResult::kDumpAndContinue) { - // Continue fuzzing after printing the crashing input. - return 0; - } - // Exit directly without invoking libFuzzer's atexit hook. - driver_cleanup(); - _Exit(Driver::kErrorExitCode); - } - return 0; -} diff --git a/driver/native_fuzzer_hooks.c b/driver/native_fuzzer_hooks.c new file mode 100644 index 00000000..4b581887 --- /dev/null +++ b/driver/native_fuzzer_hooks.c @@ -0,0 +1,527 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + * Dynamically exported definitions of fuzzer hooks and libc functions that + * forward to the symbols provided by the Jazzer driver JNI library once it has + * been loaded. + * + * Native libraries instrumented for fuzzing include references to fuzzer hooks + * that are resolved by the dynamic linker. Sanitizers such as ASan provide weak + * definitions of these symbols, but the dynamic linker doesn't distinguish + * between weak and strong symbols and thus wouldn't ever resolve them against + * the strong definitions provided by the Jazzer driver JNI library. + * Furthermore, libc functions can only be overridden in the native driver + * executable, which is the only binary that comes before the actual libc in the + * dynamic linker search order. + */ + +#define _GNU_SOURCE // for RTLD_NEXT +#include <dlfcn.h> +#include <stdatomic.h> +#include <stddef.h> +#include <string.h> + +#define GET_CALLER_PC() __builtin_return_address(0) +#define LIKELY(x) __builtin_expect(!!(x), 1) +#define UNLIKELY(x) __builtin_expect(!!(x), 0) + +typedef int (*bcmp_t)(const void *, const void *, size_t); +static _Atomic bcmp_t bcmp_real; +typedef void (*bcmp_hook_t)(void *, const void *, const void *, size_t, int); +static _Atomic bcmp_hook_t bcmp_hook; + +typedef int (*memcmp_t)(const void *, const void *, size_t); +static _Atomic memcmp_t memcmp_real; +typedef void (*memcmp_hook_t)(void *, const void *, const void *, size_t, int); +static _Atomic memcmp_hook_t memcmp_hook; + +typedef int (*strncmp_t)(const char *, const char *, size_t); +static _Atomic strncmp_t strncmp_real; +typedef void (*strncmp_hook_t)(void *, const char *, const char *, size_t, int); +static _Atomic strncmp_hook_t strncmp_hook; + +typedef int (*strcmp_t)(const char *, const char *); +static _Atomic strcmp_t strcmp_real; +typedef void (*strcmp_hook_t)(void *, const char *, const char *, int); +static _Atomic strcmp_hook_t strcmp_hook; + +typedef int (*strncasecmp_t)(const char *, const char *, size_t); +static _Atomic strncasecmp_t strncasecmp_real; +typedef void (*strncasecmp_hook_t)(void *, const char *, const char *, size_t, + int); +static _Atomic strncasecmp_hook_t strncasecmp_hook; + +typedef int (*strcasecmp_t)(const char *, const char *); +static _Atomic strcasecmp_t strcasecmp_real; +typedef void (*strcasecmp_hook_t)(void *, const char *, const char *, int); +static _Atomic strcasecmp_hook_t strcasecmp_hook; + +typedef char *(*strstr_t)(const char *, const char *); +static _Atomic strstr_t strstr_real; +typedef void (*strstr_hook_t)(void *, const char *, const char *, char *); +static _Atomic strstr_hook_t strstr_hook; + +typedef char *(*strcasestr_t)(const char *, const char *); +static _Atomic strcasestr_t strcasestr_real; +typedef void (*strcasestr_hook_t)(void *, const char *, const char *, char *); +static _Atomic strcasestr_hook_t strcasestr_hook; + +typedef void *(*memmem_t)(const void *, size_t, const void *, size_t); +static _Atomic memmem_t memmem_real; +typedef void (*memmem_hook_t)(void *, const void *, size_t, const void *, + size_t, void *); +static _Atomic memmem_hook_t memmem_hook; + +typedef void (*cov_8bit_counters_init_t)(uint8_t *, uint8_t *); +static _Atomic cov_8bit_counters_init_t cov_8bit_counters_init; +typedef void (*cov_pcs_init_t)(const uintptr_t *, const uintptr_t *); +static _Atomic cov_pcs_init_t cov_pcs_init; + +typedef void (*trace_cmp1_t)(void *, uint8_t, uint8_t); +static _Atomic trace_cmp1_t trace_cmp1_with_pc; +typedef void (*trace_cmp2_t)(void *, uint16_t, uint16_t); +static _Atomic trace_cmp2_t trace_cmp2_with_pc; +typedef void (*trace_cmp4_t)(void *, uint32_t, uint32_t); +static _Atomic trace_cmp4_t trace_cmp4_with_pc; +typedef void (*trace_cmp8_t)(void *, uint64_t, uint64_t); +static _Atomic trace_cmp8_t trace_cmp8_with_pc; + +typedef void (*trace_const_cmp1_t)(void *, uint8_t, uint8_t); +static _Atomic trace_const_cmp1_t trace_const_cmp1_with_pc; +typedef void (*trace_const_cmp2_t)(void *, uint16_t, uint16_t); +static _Atomic trace_const_cmp2_t trace_const_cmp2_with_pc; +typedef void (*trace_const_cmp4_t)(void *, uint32_t, uint32_t); +static _Atomic trace_const_cmp4_t trace_const_cmp4_with_pc; +typedef void (*trace_const_cmp8_t)(void *, uint64_t, uint64_t); +static _Atomic trace_const_cmp8_t trace_const_cmp8_with_pc; + +typedef void (*trace_switch_t)(void *, uint64_t, uint64_t *); +static _Atomic trace_switch_t trace_switch_with_pc; + +typedef void (*trace_div4_t)(void *, uint32_t); +static _Atomic trace_div4_t trace_div4_with_pc; +typedef void (*trace_div8_t)(void *, uint64_t); +static _Atomic trace_div8_t trace_div8_with_pc; + +typedef void (*trace_gep_t)(void *, uintptr_t); +static _Atomic trace_gep_t trace_gep_with_pc; + +typedef void (*trace_pc_indir_t)(void *, uintptr_t); +static _Atomic trace_pc_indir_t trace_pc_indir_with_pc; + +__attribute__((visibility("default"))) void jazzer_initialize_native_hooks( + void *handle) { + atomic_store(&bcmp_hook, dlsym(handle, "__sanitizer_weak_hook_bcmp")); + atomic_store(&memcmp_hook, dlsym(handle, "__sanitizer_weak_hook_memcmp")); + atomic_store(&strncmp_hook, dlsym(handle, "__sanitizer_weak_hook_strncmp")); + atomic_store(&strcmp_hook, dlsym(handle, "__sanitizer_weak_hook_strcmp")); + atomic_store(&strncasecmp_hook, + dlsym(handle, "__sanitizer_weak_hook_strncasecmp")); + atomic_store(&strcasecmp_hook, + dlsym(handle, "__sanitizer_weak_hook_strcasecmp")); + atomic_store(&strstr_hook, dlsym(handle, "__sanitizer_weak_hook_strstr")); + atomic_store(&strcasestr_hook, + dlsym(handle, "__sanitizer_weak_hook_strcasestr")); + atomic_store(&memmem_hook, dlsym(handle, "__sanitizer_weak_hook_memmem")); + + atomic_store(&cov_8bit_counters_init, + dlsym(handle, "__sanitizer_cov_8bit_counters_init")); + atomic_store(&cov_pcs_init, dlsym(handle, "__sanitizer_cov_pcs_init")); + + atomic_store(&trace_cmp1_with_pc, + dlsym(handle, "__sanitizer_cov_trace_cmp1_with_pc")); + atomic_store(&trace_cmp2_with_pc, + dlsym(handle, "__sanitizer_cov_trace_cmp2_with_pc")); + atomic_store(&trace_cmp4_with_pc, + dlsym(handle, "__sanitizer_cov_trace_cmp4_with_pc")); + atomic_store(&trace_cmp8_with_pc, + dlsym(handle, "__sanitizer_cov_trace_cmp8_with_pc")); + + atomic_store(&trace_const_cmp1_with_pc, + dlsym(handle, "__sanitizer_cov_trace_const_cmp1_with_pc")); + atomic_store(&trace_const_cmp2_with_pc, + dlsym(handle, "__sanitizer_cov_trace_const_cmp2_with_pc")); + atomic_store(&trace_const_cmp4_with_pc, + dlsym(handle, "__sanitizer_cov_trace_const_cmp4_with_pc")); + atomic_store(&trace_const_cmp8_with_pc, + dlsym(handle, "__sanitizer_cov_trace_const_cmp8_with_pc")); + + atomic_store(&trace_switch_with_pc, + dlsym(handle, "__sanitizer_cov_trace_switch_with_pc")); + + atomic_store(&trace_div4_with_pc, + dlsym(handle, "__sanitizer_cov_trace_div4_with_pc")); + atomic_store(&trace_div8_with_pc, + dlsym(handle, "__sanitizer_cov_trace_div8_with_pc")); + + atomic_store(&trace_gep_with_pc, + dlsym(handle, "__sanitizer_cov_trace_gep_with_pc")); + + atomic_store(&trace_pc_indir_with_pc, + dlsym(handle, "__sanitizer_cov_trace_pc_indir_with_pc")); +} + +// Alternate definitions for libc functions mimicking those that libFuzzer would +// provide if it were part of the native driver executable. All these functions +// invoke the real libc function loaded from the next library in search order +// (usually libc itself). +// Function pointers have to be loaded and stored atomically even if libc +// functions are invoked from different threads, but we do not need any +// synchronization guarantees - in the worst case, we will non-deterministically +// lose a few hook invocations. + +__attribute__((visibility("default"))) int bcmp(const void *s1, const void *s2, + size_t n) { + bcmp_t bcmp_real_local = + atomic_load_explicit(&bcmp_real, memory_order_relaxed); + if (UNLIKELY(bcmp_real_local == NULL)) { + bcmp_real_local = dlsym(RTLD_NEXT, "bcmp"); + atomic_store_explicit(&bcmp_real, bcmp_real_local, memory_order_relaxed); + } + + int result = bcmp_real_local(s1, s2, n); + bcmp_hook_t hook = atomic_load_explicit(&bcmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, n, result); + } + return result; +} + +__attribute__((visibility("default"))) int memcmp(const void *s1, + const void *s2, size_t n) { + memcmp_t memcmp_real_local = + atomic_load_explicit(&memcmp_real, memory_order_relaxed); + if (UNLIKELY(memcmp_real_local == NULL)) { + memcmp_real_local = dlsym(RTLD_NEXT, "memcmp"); + atomic_store_explicit(&memcmp_real, memcmp_real_local, + memory_order_relaxed); + } + + int result = memcmp_real_local(s1, s2, n); + memcmp_hook_t hook = atomic_load_explicit(&memcmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, n, result); + } + return result; +} + +__attribute__((visibility("default"))) int strncmp(const char *s1, + const char *s2, size_t n) { + strncmp_t strncmp_real_local = + atomic_load_explicit(&strncmp_real, memory_order_relaxed); + if (UNLIKELY(strncmp_real_local == NULL)) { + strncmp_real_local = dlsym(RTLD_NEXT, "strncmp"); + atomic_store_explicit(&strncmp_real, strncmp_real_local, + memory_order_relaxed); + } + + int result = strncmp_real_local(s1, s2, n); + strncmp_hook_t hook = + atomic_load_explicit(&strncmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, n, result); + } + return result; +} + +__attribute__((visibility("default"))) int strncasecmp(const char *s1, + const char *s2, + size_t n) { + strncasecmp_t strncasecmp_real_local = + atomic_load_explicit(&strncasecmp_real, memory_order_relaxed); + if (UNLIKELY(strncasecmp_real_local == NULL)) { + strncasecmp_real_local = dlsym(RTLD_NEXT, "strncasecmp"); + atomic_store_explicit(&strncasecmp_real, strncasecmp_real_local, + memory_order_relaxed); + } + + int result = strncasecmp_real_local(s1, s2, n); + strncasecmp_hook_t hook = + atomic_load_explicit(&strncasecmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, n, result); + } + return result; +} + +__attribute__((visibility("default"))) int strcmp(const char *s1, + const char *s2) { + strcmp_t strcmp_real_local = + atomic_load_explicit(&strcmp_real, memory_order_relaxed); + if (UNLIKELY(strcmp_real_local == NULL)) { + strcmp_real_local = dlsym(RTLD_NEXT, "strcmp"); + atomic_store_explicit(&strcmp_real, strcmp_real_local, + memory_order_relaxed); + } + + int result = strcmp_real_local(s1, s2); + strcmp_hook_t hook = atomic_load_explicit(&strcmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, result); + } + return result; +} + +__attribute__((visibility("default"))) int strcasecmp(const char *s1, + const char *s2) { + strcasecmp_t strcasecmp_real_local = + atomic_load_explicit(&strcasecmp_real, memory_order_relaxed); + if (UNLIKELY(strcasecmp_real_local == NULL)) { + strcasecmp_real_local = dlsym(RTLD_NEXT, "strcasecmp"); + atomic_store_explicit(&strcasecmp_real, strcasecmp_real_local, + memory_order_relaxed); + } + + int result = strcasecmp_real_local(s1, s2); + strcasecmp_hook_t hook = + atomic_load_explicit(&strcasecmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, result); + } + return result; +} + +__attribute__((visibility("default"))) char *strstr(const char *s1, + const char *s2) { + strstr_t strstr_real_local = + atomic_load_explicit(&strstr_real, memory_order_relaxed); + if (UNLIKELY(strstr_real_local == NULL)) { + strstr_real_local = dlsym(RTLD_NEXT, "strstr"); + atomic_store_explicit(&strstr_real, strstr_real_local, + memory_order_relaxed); + } + + char *result = strstr_real_local(s1, s2); + strstr_hook_t hook = atomic_load_explicit(&strstr_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, result); + } + return result; +} + +__attribute__((visibility("default"))) char *strcasestr(const char *s1, + const char *s2) { + strcasestr_t strcasestr_real_local = + atomic_load_explicit(&strcasestr_real, memory_order_relaxed); + if (UNLIKELY(strcasestr_real_local == NULL)) { + strcasestr_real_local = dlsym(RTLD_NEXT, "strcasestr"); + atomic_store_explicit(&strcasestr_real, strcasestr_real_local, + memory_order_relaxed); + } + + char *result = strcasestr_real_local(s1, s2); + strcasestr_hook_t hook = + atomic_load_explicit(&strcasestr_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, s2, result); + } + return result; +} + +__attribute__((visibility("default"))) void *memmem(const void *s1, size_t n1, + const void *s2, size_t n2) { + memmem_t memmem_real_local = + atomic_load_explicit(&memmem_real, memory_order_relaxed); + if (UNLIKELY(memmem_real_local == NULL)) { + memmem_real_local = dlsym(RTLD_NEXT, "memmem"); + atomic_store_explicit(&memmem_real, memmem_real_local, + memory_order_relaxed); + } + + void *result = memmem_real_local(s1, n1, s2, n2); + memmem_hook_t hook = atomic_load_explicit(&memmem_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(GET_CALLER_PC(), s1, n1, s2, n2, result); + } + return result; +} + +// The __sanitizer_cov_trace_* family of functions is only invoked from code +// compiled with -fsanitize=fuzzer. We can assume that the Jazzer JNI library +// has been loaded before any such code, which necessarily belongs to the fuzz +// target, is executed and thus don't need NULL checks. + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_cmp1( + uint8_t arg1, uint8_t arg2) { + trace_cmp1_t hook = + atomic_load_explicit(&trace_cmp1_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_cmp2( + uint16_t arg1, uint16_t arg2) { + trace_cmp2_t hook = + atomic_load_explicit(&trace_cmp2_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_cmp4( + uint32_t arg1, uint32_t arg2) { + trace_cmp4_t hook = + atomic_load_explicit(&trace_cmp4_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_cmp8( + uint64_t arg1, uint64_t arg2) { + trace_cmp8_t hook = + atomic_load_explicit(&trace_cmp8_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_const_cmp1( + uint8_t arg1, uint8_t arg2) { + trace_const_cmp1_t hook = + atomic_load_explicit(&trace_const_cmp1_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_const_cmp2( + uint16_t arg1, uint16_t arg2) { + trace_const_cmp2_t hook = + atomic_load_explicit(&trace_const_cmp2_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_const_cmp4( + uint32_t arg1, uint32_t arg2) { + trace_const_cmp4_t hook = + atomic_load_explicit(&trace_const_cmp4_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_const_cmp8( + uint64_t arg1, uint64_t arg2) { + trace_const_cmp8_t hook = + atomic_load_explicit(&trace_const_cmp8_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), arg1, arg2); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_switch( + uint64_t val, uint64_t *cases) { + trace_switch_t hook = + atomic_load_explicit(&trace_switch_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), val, cases); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_div4( + uint32_t val) { + trace_div4_t hook = + atomic_load_explicit(&trace_div4_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), val); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_div8( + uint64_t val) { + trace_div8_t hook = + atomic_load_explicit(&trace_div8_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), val); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_gep( + uintptr_t idx) { + trace_gep_t hook = + atomic_load_explicit(&trace_gep_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), idx); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_trace_pc_indir( + uintptr_t callee) { + trace_pc_indir_t hook = + atomic_load_explicit(&trace_pc_indir_with_pc, memory_order_relaxed); + hook(GET_CALLER_PC(), callee); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_8bit_counters_init( + uint8_t *start, uint8_t *end) { + cov_8bit_counters_init_t init = + atomic_load_explicit(&cov_8bit_counters_init, memory_order_relaxed); + init(start, end); +} + +__attribute__((visibility("default"))) void __sanitizer_cov_pcs_init( + const uintptr_t *pcs_beg, const uintptr_t *pcs_end) { + cov_pcs_init_t init = + atomic_load_explicit(&cov_pcs_init, memory_order_relaxed); + init(pcs_beg, pcs_end); +} + +// The __sanitizer_weak_hook_* family of functions can be invoked early on macOS +// and thus requires NULL checks. + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_memcmp( + void *called_pc, const void *s1, const void *s2, size_t n, int result) { + memcmp_hook_t hook = atomic_load_explicit(&memcmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, n, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strncmp( + void *called_pc, const void *s1, const void *s2, size_t n, int result) { + strncmp_hook_t hook = + atomic_load_explicit(&strncmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, n, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strcmp( + void *called_pc, const void *s1, const void *s2, int result) { + strcmp_hook_t hook = atomic_load_explicit(&strcmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strncasecmp( + void *called_pc, const void *s1, const void *s2, size_t n, int result) { + strncasecmp_hook_t hook = + atomic_load_explicit(&strncasecmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, n, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strcasecmp( + void *called_pc, const void *s1, const void *s2, int result) { + strcasecmp_hook_t hook = + atomic_load_explicit(&strcasecmp_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strstr( + void *called_pc, const void *s1, const void *s2, char *result) { + strstr_hook_t hook = atomic_load_explicit(&strstr_hook, memory_order_relaxed); + if (LIKELY(hook != NULL)) { + hook(called_pc, s1, s2, result); + } +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_strcasestr( + void *called_pc, const void *s1, const void *s2, char *result) { + strcasestr_hook_t hook = + atomic_load_explicit(&strstr_hook, memory_order_relaxed); + hook(called_pc, s1, s2, result); +} + +__attribute__((visibility("default"))) void __sanitizer_weak_hook_memmem( + void *called_pc, const void *s1, size_t len1, const void *s2, size_t len2, + void *result) { + memmem_hook_t hook = atomic_load_explicit(&memmem_hook, memory_order_relaxed); + hook(called_pc, s1, len1, s2, len2, result); +} diff --git a/driver/sanitizer_hooks_with_pc.cpp b/driver/sanitizer_hooks_with_pc.cpp deleted file mode 100644 index bb3ec5e1..00000000 --- a/driver/sanitizer_hooks_with_pc.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "sanitizer_hooks_with_pc.h" - -#include <cstddef> -#include <cstdint> - -// libFuzzer's compare hooks obtain the caller's address from the compiler -// builtin __builtin_return_adress. Since Java code will invoke the hooks always -// from the same native function, this builtin would always return the same -// value. Internally, the libFuzzer hooks call through to the always inlined -// HandleCmp and thus can't be mimicked without patching libFuzzer. -// -// We solve this problem via an inline assembly trampoline construction that -// translates a runtime argument `fake_pc` in the range [0, 512) into a call to -// a hook with a fake return address whose lower 9 bits are `fake_pc` up to a -// constant shift. This is achieved by pushing a return address pointing into -// 512 ret instructions at offset `fake_pc` onto the stack and then jumping -// directly to the address of the hook. -// -// Note: We only set the lowest 9 bits of the return address since only these -// bits are used by the libFuzzer value profiling mode for integer compares, see -// https://github.com/llvm/llvm-project/blob/704d92607d26e696daba596b72cb70effe79a872/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp#L390 -// as well as -// https://github.com/llvm/llvm-project/blob/704d92607d26e696daba596b72cb70effe79a872/compiler-rt/lib/fuzzer/FuzzerValueBitMap.h#L34 -// ValueProfileMap.AddValue() truncates its argument to 16 bits and shifts the -// PC to the left by log_2(128)=7, which means that only the lowest 16 - 7 bits -// of the return address matter. String compare hooks use the lowest 12 bits, -// but take the return address as an argument and thus don't require the -// indirection through a trampoline. - -#define REPEAT_8(a) a a a a a a a a - -#define REPEAT_512(a) REPEAT_8(REPEAT_8(REPEAT_8(a))) - -// The first four registers to pass arguments in according to the -// platform-specific x64 calling convention. -#ifdef _WIN64 -#define REG_1 "rcx" -#define REG_2 "rdx" -#define REG_3 "r8" -#define REG_4 "r9" -#else -#define REG_1 "rdi" -#define REG_2 "rsi" -#define REG_3 "rdx" -#define REG_4 "rcx" -#endif - -// Call the function at address `func` with arguments `arg1` and `arg2` while -// ensuring that the return address is `fake_pc` up to a globally constant -// offset. -__attribute__((noinline)) void trampoline(uint64_t arg1, uint64_t arg2, - void *func, uint16_t fake_pc) { - // arg1 and arg2 have to be forwarded according to the x64 calling convention. - // We also fix func and fake_pc to their registers so that we can safely use - // rax below. - [[maybe_unused]] register uint64_t arg1_loc asm(REG_1) = arg1; - [[maybe_unused]] register uint64_t arg2_loc asm(REG_2) = arg2; - [[maybe_unused]] register void *func_loc asm(REG_3) = func; - [[maybe_unused]] register uint64_t fake_pc_loc asm(REG_4) = fake_pc; - asm volatile goto( - // Load RIP-relative address of the end of this function. - "lea %l[end_of_function](%%rip), %%rax \n\t" - "push %%rax \n\t" - // Load RIP-relative address of the ret sled into rax. - "lea ret_sled(%%rip), %%rax \n\t" - // Add the offset of the fake_pc-th ret. - "add %[fake_pc], %%rax \n\t" - // Push the fake return address pointing to that ret. The hook will return - // to it and then immediately return to the end of this function. - "push %%rax \n\t" - // Call func with the fake return address on the stack. - // Function arguments arg1 and arg2 are passed unchanged in the registers - // RDI and RSI as governed by the x64 calling convention. - "jmp *%[func] \n\t" - // Append a sled of 2^9=512 ret instructions. - "ret_sled: \n\t" REPEAT_512("ret \n\t") - : - : "r"(arg1_loc), - "r"(arg2_loc), [func] "r"(func_loc), [fake_pc] "r"(fake_pc_loc) - : "memory" - : end_of_function); - -end_of_function: - return; -} - -namespace { -uintptr_t trampoline_offset = 0; -} - -void set_trampoline_offset() { - // Stores the additive inverse of the current return address modulo 0x200u in - // trampoline_offset. - trampoline_offset = - 0x200u - - (reinterpret_cast<uintptr_t>(__builtin_return_address(0)) & 0x1FFu); -} - -// Computes the additive shift that needs to be applied to the caller PC by -// caller_pc_to_fake_pc to make caller PC and resulting fake return address -// in their lowest 9 bite. This offset is constant for each binary, but may vary -// based on code generation specifics. By calibrating the trampoline, the fuzzer -// behavior is fully determined by the seed. -void CalibrateTrampoline() { - trampoline(0, 0, reinterpret_cast<void *>(&set_trampoline_offset), 0); -} - -// Masks any address down to its lower 9 bits, adjusting for the trampoline -// shift. -__attribute__((always_inline)) inline uint16_t caller_pc_to_fake_pc( - const void *caller_pc) { - return (reinterpret_cast<uintptr_t>(caller_pc) + trampoline_offset) & 0x1FFu; -} - -// The original hooks exposed by libFuzzer. All of these get the caller's -// address via __builtin_return_address(0). -extern "C" { -void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2); -void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2); -void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases); -void __sanitizer_cov_trace_div4(uint32_t val); -void __sanitizer_cov_trace_div8(uint64_t val); -void __sanitizer_cov_trace_gep(uintptr_t idx); -void __sanitizer_cov_trace_pc_indir(uintptr_t callee); -} -void __sanitizer_cov_trace_cmp4_with_pc(void *caller_pc, uint32_t arg1, - uint32_t arg2) { - void *trace_cmp4 = reinterpret_cast<void *>(&__sanitizer_cov_trace_cmp4); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(arg1), static_cast<uint64_t>(arg2), - trace_cmp4, fake_pc); -} - -void __sanitizer_cov_trace_cmp8_with_pc(void *caller_pc, uint64_t arg1, - uint64_t arg2) { - void *trace_cmp8 = reinterpret_cast<void *>(&__sanitizer_cov_trace_cmp8); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(arg1), static_cast<uint64_t>(arg2), - trace_cmp8, fake_pc); -} - -void __sanitizer_cov_trace_switch_with_pc(void *caller_pc, uint64_t val, - uint64_t *cases) { - void *trace_switch = reinterpret_cast<void *>(&__sanitizer_cov_trace_switch); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(val), reinterpret_cast<uint64_t>(cases), - trace_switch, fake_pc); -} - -void __sanitizer_cov_trace_div4_with_pc(void *caller_pc, uint32_t val) { - void *trace_div4 = reinterpret_cast<void *>(&__sanitizer_cov_trace_div4); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(val), 0, trace_div4, fake_pc); -} - -void __sanitizer_cov_trace_div8_with_pc(void *caller_pc, uint64_t val) { - void *trace_div8 = reinterpret_cast<void *>(&__sanitizer_cov_trace_div8); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(val), 0, trace_div8, fake_pc); -} - -void __sanitizer_cov_trace_gep_with_pc(void *caller_pc, uintptr_t idx) { - void *trace_gep = reinterpret_cast<void *>(&__sanitizer_cov_trace_gep); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(idx), 0, trace_gep, fake_pc); -} - -void __sanitizer_cov_trace_pc_indir_with_pc(void *caller_pc, uintptr_t callee) { - void *trace_pc_indir = - reinterpret_cast<void *>(&__sanitizer_cov_trace_pc_indir); - auto fake_pc = caller_pc_to_fake_pc(caller_pc); - trampoline(static_cast<uint64_t>(callee), 0, trace_pc_indir, fake_pc); -} diff --git a/driver/sanitizer_hooks_with_pc_test.cpp b/driver/sanitizer_hooks_with_pc_test.cpp deleted file mode 100644 index 71d1527b..00000000 --- a/driver/sanitizer_hooks_with_pc_test.cpp +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "sanitizer_hooks_with_pc.h" - -#include <algorithm> -#include <cmath> -#include <cstdint> -#include <iostream> - -#include "gtest/gtest.h" - -static std::vector<uint16_t> gCoverageMap(512); - -inline void __attribute__((always_inline)) RecordCoverage() { - auto return_address = - reinterpret_cast<uintptr_t>(__builtin_return_address(0)); - auto idx = return_address & (gCoverageMap.size() - 1); - gCoverageMap[idx]++; -} - -extern "C" { -void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2) { - RecordCoverage(); -} - -void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2) { - RecordCoverage(); -} - -void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases) { - RecordCoverage(); -} - -void __sanitizer_cov_trace_div4(uint32_t val) { RecordCoverage(); } - -void __sanitizer_cov_trace_div8(uint64_t val) { RecordCoverage(); } - -void __sanitizer_cov_trace_gep(uintptr_t idx) { RecordCoverage(); } - -void __sanitizer_cov_trace_pc_indir(uintptr_t callee) { RecordCoverage(); } -} - -void ClearCoverage() { std::fill(gCoverageMap.begin(), gCoverageMap.end(), 0); } - -bool HasAllPcsCovered() { - return 0 == std::count(gCoverageMap.cbegin(), gCoverageMap.cend(), 0); -} - -bool HasSingleCoveredPc() { - return gCoverageMap.size() - 1 == - std::count(gCoverageMap.cbegin(), gCoverageMap.cend(), 0); -} - -std::string PrettyPrintCoverage() { - std::ostringstream out; - std::size_t break_after = 16; - out << "Coverage:" << std::endl; - for (uintptr_t i = 0; i < gCoverageMap.size(); i++) { - out << (gCoverageMap[i] ? "X" : "_"); - if (i % break_after == break_after - 1) out << std::endl; - } - return out.str(); -} - -class TestFakePcTrampoline : public ::testing::Test { - protected: - TestFakePcTrampoline() { - ClearCoverage(); - CalibrateTrampoline(); - } -}; - -TEST_F(TestFakePcTrampoline, TraceCmp4Direct) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_cmp4(i, i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceCmp8Direct) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_cmp8(i, i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceSwitchDirect) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_switch(i, nullptr); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceDiv4Direct) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_div4(i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceDiv8Direct) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_div8(i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceGepDirect) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_gep(i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TracePcIndirDirect) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_pc_indir(i); - } - EXPECT_TRUE(HasSingleCoveredPc()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceCmp4Trampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_cmp4_with_pc(reinterpret_cast<void *>(i), i, i); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceCmp8Trampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_cmp8_with_pc(reinterpret_cast<void *>(i), i, i); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceSwitchTrampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_switch_with_pc(reinterpret_cast<void *>(i), i, - nullptr); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceDiv4Trampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_div4_with_pc(reinterpret_cast<void *>(i), i); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceDiv8Trampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_div8_with_pc(reinterpret_cast<void *>(i), i); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TraceGepTrampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_gep_with_pc(reinterpret_cast<void *>(i), i); - EXPECT_EQ(1, gCoverageMap[i]); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} - -TEST_F(TestFakePcTrampoline, TracePcIndirTrampoline) { - for (uint32_t i = 0; i < gCoverageMap.size(); ++i) { - __sanitizer_cov_trace_pc_indir_with_pc(reinterpret_cast<void *>(i), i); - } - EXPECT_TRUE(HasAllPcsCovered()) << PrettyPrintCoverage(); -} diff --git a/driver/sanitizer_symbols_for_tests.cpp b/driver/sanitizer_symbols_for_tests.cpp deleted file mode 100644 index 7d84feac..00000000 --- a/driver/sanitizer_symbols_for_tests.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include <cstddef> -#include <cstdint> - -// Symbols exported by libFuzzer that are required by libfuzzer_callbacks and -// CoverageTracker. -extern "C" { -void __sanitizer_cov_8bit_counters_init(uint8_t *start, uint8_t *end) {} -void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, - const uintptr_t *pcs_end) {} -size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries) { - *pc_entries = new uintptr_t[0]; - return 0; -} -void __sanitizer_weak_hook_memcmp(void *caller_pc, const void *s1, - const void *s2, std::size_t n, int result) {} -void __sanitizer_weak_hook_strcmp(void *caller_pc, const char *s1, - const char *s2, int result) {} -void __sanitizer_weak_hook_compare_bytes(void *caller_pc, const void *s1, - const void *s2, std::size_t n1, - std::size_t n2, int result) {} -void __sanitizer_weak_hook_strstr(void *caller_pc, const char *s1, - const char *s2, int result) {} -void __sanitizer_cov_trace_cmp4(uint32_t arg1, uint32_t arg2) {} -void __sanitizer_cov_trace_cmp8(uint64_t arg1, uint64_t arg2) {} -void __sanitizer_cov_trace_switch(uint64_t val, uint64_t *cases) {} -void __sanitizer_cov_trace_div4(uint32_t val) {} -void __sanitizer_cov_trace_div8(uint64_t val) {} -void __sanitizer_cov_trace_gep(uintptr_t idx) {} -void __sanitizer_cov_trace_pc_indir(uintptr_t callee) {} -void __sanitizer_set_death_callback(void (*callback)()) {} -} diff --git a/driver/signal_handler.cpp b/driver/signal_handler.cpp deleted file mode 100644 index 05e5953a..00000000 --- a/driver/signal_handler.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "signal_handler.h" - -#include <jni.h> - -#include <atomic> -#include <csignal> -#include <stdexcept> - -constexpr auto kSignalHandlerClass = - "com/code_intelligence/jazzer/runtime/SignalHandler"; - -// Handles SIGINT raised while running Java code. -void JNICALL handleInterrupt(JNIEnv, jclass) { - static std::atomic<bool> already_exiting{false}; - if (!already_exiting.exchange(true)) { - // Let libFuzzer exit gracefully when the JVM received SIGINT. - raise(SIGUSR1); - } else { - // Exit libFuzzer forcefully on repeated SIGINTs. - raise(SIGTERM); - } -} - -namespace jazzer { -void SignalHandler::Setup(JNIEnv &env) { - jclass signal_handler_class = env.FindClass(kSignalHandlerClass); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error("could not find signal handler class"); - } - JNINativeMethod signal_handler_methods[]{ - {(char *)"handleInterrupt", (char *)"()V", (void *)&handleInterrupt}, - }; - env.RegisterNatives(signal_handler_class, signal_handler_methods, 1); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error( - "could not register native callbacks 'handleInterrupt'"); - } - jmethodID setup_signal_handlers_method_ = - env.GetStaticMethodID(signal_handler_class, "setupSignalHandlers", "()V"); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error("could not find setupSignalHandlers method"); - } - env.CallStaticVoidMethod(signal_handler_class, setup_signal_handlers_method_); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error("failed to set up signal handlers"); - } -} -} // namespace jazzer diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel new file mode 100644 index 00000000..c8e6ba1e --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -0,0 +1,64 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") + +java_library( + name = "driver", + srcs = [":Driver.java"], + visibility = [ + "//agent:__pkg__", + ], + deps = [ + ":fuzz_target_runner", + ":opt", + ":utils", + "//agent/src/main/java/com/code_intelligence/jazzer/agent:agent_lib", + "@net_bytebuddy_byte_buddy_agent//jar", + ], +) + +java_jni_library( + name = "fuzz_target_runner", + srcs = ["FuzzTargetRunner.java"], + native_libs = [ + "//driver/src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver", + ], + visibility = [ + "//agent:__pkg__", + "//driver/src/main/native/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/test:__subpackages__", + ], + deps = [ + ":opt", + ":reproducer_template", + ":utils", + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/autofuzz", + "//agent/src/main/java/com/code_intelligence/jazzer/instrumentor", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:signal_handler", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider", + "//agent/src/main/java/com/code_intelligence/jazzer/utils", + ], +) + +java_library( + name = "reproducer_template", + srcs = ["ReproducerTemplate.java"], + resources = ["Reproducer.java.tmpl"], + deps = [":opt"], +) + +java_library( + name = "opt", + srcs = ["Opt.java"], + visibility = [ + "//agent/src/main/java/com/code_intelligence/jazzer:__subpackages__", + "//driver/src/test/java/com/code_intelligence/jazzer/driver:__pkg__", + ], +) + +java_library( + name = "utils", + srcs = ["Utils.java"], +) diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java new file mode 100644 index 00000000..5b107ad8 --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -0,0 +1,111 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import static java.lang.System.err; + +import com.code_intelligence.jazzer.agent.Agent; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.List; +import net.bytebuddy.agent.ByteBuddyAgent; + +public class Driver { + // Accessed from jazzer_main.cpp. + @SuppressWarnings("unused") + private static int start(byte[][] nativeArgs) throws IOException { + List<String> args = Utils.fromNativeArgs(nativeArgs); + + final boolean spawnsSubprocesses = args.stream().anyMatch( + arg -> arg.startsWith("-fork=") || arg.startsWith("-jobs=") || arg.startsWith("-merge=")); + if (spawnsSubprocesses) { + if (!System.getProperty("jazzer.coverage_report", "").isEmpty()) { + err.println( + "WARN: --coverage_report does not support parallel fuzzing and has been disabled"); + System.clearProperty("jazzer.coverage_report"); + } + if (!System.getProperty("jazzer.coverage_dump", "").isEmpty()) { + err.println( + "WARN: --coverage_dump does not support parallel fuzzing and has been disabled"); + System.clearProperty("jazzer.coverage_dump"); + } + + String idSyncFileArg = System.getProperty("jazzer.id_sync_file", ""); + Path idSyncFile; + if (idSyncFileArg.isEmpty()) { + // Create an empty temporary file used for coverage ID synchronization and + // pass its path to the agent in every child process. This requires adding + // the argument to argv for it to be picked up by libFuzzer, which then + // forwards it to child processes. + idSyncFile = Files.createTempFile("jazzer-", ""); + args.add("--id_sync_file=" + idSyncFile.toAbsolutePath()); + } else { + // Creates the file, truncating it if it exists. + idSyncFile = Files.write(Paths.get(idSyncFileArg), new byte[] {}); + } + // This wouldn't run in case we exit the process with _Exit, but the parent process of a -fork + // run is expected to exit with a regular exit(0), which does cause JVM shutdown hooks to run: + // https://github.com/llvm/llvm-project/blob/940e178c0018b32af2f1478d331fc41a92a7dac7/compiler-rt/lib/fuzzer/FuzzerFork.cpp#L491 + idSyncFile.toFile().deleteOnExit(); + } + + // Jazzer's hooks use deterministic randomness and thus require a seed. Search for the last + // occurrence of a "-seed" argument as that is the one that is used by libFuzzer. If none is + // set, generate one and pass it to libFuzzer so that a fuzzing run can be reproduced simply by + // setting the seed printed by libFuzzer. + String seed = args.stream().reduce( + null, (prev, cur) -> cur.startsWith("-seed=") ? cur.substring("-seed=".length()) : prev); + if (seed == null) { + seed = Integer.toUnsignedString(new SecureRandom().nextInt()); + // Only add the -seed argument to the command line if not running in a mode + // that spawns subprocesses. These would inherit the same seed, which might + // make them less effective. + if (!spawnsSubprocesses) { + args.add("-seed=" + seed); + } + } + System.setProperty("jazzer.seed", seed); + + if (args.stream().noneMatch(arg -> arg.startsWith("-rss_limit_mb="))) { + args.add(getDefaultRssLimitMbArg()); + } + + // Do *not* modify system properties beyond this point - initializing Opt parses them as a side + // effect. + + if (Opt.hooks) { + Agent.premain(null, ByteBuddyAgent.install()); + } + + return FuzzTargetRunner.startLibFuzzer(args); + } + + private static String getDefaultRssLimitMbArg() { + // Java OutOfMemoryErrors are strictly more informative than libFuzzer's out of memory crashes. + // We thus want to scale the default libFuzzer memory limit, which includes all memory used by + // the process including Jazzer's native and non-native memory footprint, such that: + // 1. we never reach it purely by allocating memory on the Java heap; + // 2. it is still reached if the fuzz target allocates excessively on the native heap. + // As a heuristic, we set the overall memory limit to 2 * the maximum size of the Java heap and + // add a fixed 1 GiB on top for the fuzzer's own memory usage. + long maxHeapInBytes = Runtime.getRuntime().maxMemory(); + return "-rss_limit_mb=" + ((2 * maxHeapInBytes / (1024 * 1024)) + 1024); + } +} diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java new file mode 100644 index 00000000..5646e91a --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java @@ -0,0 +1,450 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import static java.lang.System.err; +import static java.lang.System.exit; +import static java.lang.System.out; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.autofuzz.FuzzTarget; +import com.code_intelligence.jazzer.instrumentor.CoverageRecorder; +import com.code_intelligence.jazzer.runtime.CoverageMap; +import com.code_intelligence.jazzer.runtime.FuzzedDataProviderImpl; +import com.code_intelligence.jazzer.runtime.JazzerInternal; +import com.code_intelligence.jazzer.runtime.RecordingFuzzedDataProvider; +import com.code_intelligence.jazzer.runtime.SignalHandler; +import com.code_intelligence.jazzer.runtime.UnsafeProvider; +import com.code_intelligence.jazzer.utils.ExceptionUtils; +import com.code_intelligence.jazzer.utils.ManifestUtils; +import com.github.fmeum.rules_jni.RulesJni; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import sun.misc.Unsafe; + +/** + * Executes a fuzz target and reports findings. + * + * <p>This class maintains global state (both native and non-native) and thus cannot be used + * concurrently. + */ +public final class FuzzTargetRunner { + static { + RulesJni.loadLibrary("jazzer_driver", FuzzTargetRunner.class); + } + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final long BYTE_ARRAY_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + + // Default value of the libFuzzer -error_exitcode flag. + private static final int LIBFUZZER_ERROR_EXIT_CODE = 77; + private static final String AUTOFUZZ_FUZZ_TARGET = + "com.code_intelligence.jazzer.autofuzz.FuzzTarget"; + private static final String FUZZER_TEST_ONE_INPUT = "fuzzerTestOneInput"; + private static final String FUZZER_INITIALIZE = "fuzzerInitialize"; + private static final String FUZZER_TEARDOWN = "fuzzerTearDown"; + + private static final Set<Long> ignoredTokens = new HashSet<>(Opt.ignore); + private static final FuzzedDataProviderImpl fuzzedDataProvider = + FuzzedDataProviderImpl.withNativeData(); + private static final Class<?> fuzzTargetClass; + private static final MethodHandle fuzzTarget; + public static final boolean useFuzzedDataProvider; + private static final ReproducerTemplate reproducerTemplate; + + static { + String targetClassName = determineFuzzTargetClassName(); + + // FuzzTargetRunner is loaded by the bootstrap class loader since Driver installs the agent + // before invoking FuzzTargetRunner.startLibFuzzer. We can't load the fuzz target with that + // class loader - we have to use the class loader that loaded Driver. This would be + // straightforward to do in Java 9+, but requires the use of reflection to maintain + // compatibility with Java 8, which doesn't have StackWalker. + // + // Note that we can't just move the agent initialization so that FuzzTargetRunner is loaded by + // Driver's class loader: The agent and FuzzTargetRunner have to share the native library that + // contains libFuzzer and that library needs to be available in the bootstrap class loader + // since instrumentation applied to Java standard library classes still needs to be able to call + // libFuzzer hooks. A fundamental JNI restriction is that a native library can't be shared + // between two different class loaders, so FuzzTargetRunner is thus forced to be loaded in the + // bootstrap class loader, which makes this ugly code block necessary. + // We also can't use the system class loader since Driver may be loaded by a custom class loader + // if not invoked from the native driver. + Class<?> driverClass; + try { + Class<?> reflectionClass = Class.forName("sun.reflect.Reflection"); + try { + driverClass = + (Class<?>) reflectionClass.getMethod("getCallerClass", int.class).invoke(null, 2); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } catch (ClassNotFoundException e) { + // sun.reflect.Reflection is no longer available after Java 8, use StackWalker. + try { + Class<?> stackWalker = Class.forName("java.lang.StackWalker"); + Class<? extends Enum<?>> stackWalkerOption = + (Class<? extends Enum<?>>) Class.forName("java.lang.StackWalker$Option"); + Enum<?> retainClassReferences = + Arrays.stream(stackWalkerOption.getEnumConstants()) + .filter(v -> v.name().equals("RETAIN_CLASS_REFERENCE")) + .findFirst() + .orElseThrow(() + -> new IllegalStateException( + "No RETAIN_CLASS_REFERENCE in java.lang.StackWalker$Option")); + Object stackWalkerInstance = stackWalker.getMethod("getInstance", stackWalkerOption) + .invoke(null, retainClassReferences); + Method stackWalkerGetCallerClass = stackWalker.getMethod("getCallerClass"); + driverClass = (Class<?>) stackWalkerGetCallerClass.invoke(stackWalkerInstance); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException ex) { + throw new IllegalStateException(ex); + } + } + + try { + ClassLoader driverClassLoader = driverClass.getClassLoader(); + driverClassLoader.setDefaultAssertionStatus(true); + fuzzTargetClass = Class.forName(targetClassName, false, driverClassLoader); + } catch (ClassNotFoundException e) { + err.print("ERROR: "); + e.printStackTrace(err); + exit(1); + throw new IllegalStateException("Not reached"); + } + // Inform the agent about the fuzz target class. Important note: This has to be done *before* + // the class is initialized so that hooks can enable themselves in time for the fuzz target's + // static initializer. + JazzerInternal.onFuzzTargetReady(targetClassName); + + Method bytesFuzzTarget = targetPublicStaticMethodOrNull(FUZZER_TEST_ONE_INPUT, byte[].class); + Method dataFuzzTarget = + targetPublicStaticMethodOrNull(FUZZER_TEST_ONE_INPUT, FuzzedDataProvider.class); + if ((bytesFuzzTarget != null) == (dataFuzzTarget != null)) { + err.printf( + "ERROR: %s must define exactly one of the following two functions:%n", targetClassName); + err.println("public static void fuzzerTestOneInput(byte[] ...)"); + err.println("public static void fuzzerTestOneInput(FuzzedDataProvider ...)"); + err.println( + "Note: Fuzz targets returning boolean are no longer supported; exceptions should be thrown instead of returning true."); + exit(1); + } + try { + if (bytesFuzzTarget != null) { + useFuzzedDataProvider = false; + fuzzTarget = MethodHandles.publicLookup().unreflect(bytesFuzzTarget); + } else { + useFuzzedDataProvider = true; + fuzzTarget = MethodHandles.publicLookup().unreflect(dataFuzzTarget); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + reproducerTemplate = new ReproducerTemplate(fuzzTargetClass.getName(), useFuzzedDataProvider); + + Method initializeNoArgs = targetPublicStaticMethodOrNull(FUZZER_INITIALIZE); + Method initializeWithArgs = targetPublicStaticMethodOrNull(FUZZER_INITIALIZE, String[].class); + try { + if (initializeWithArgs != null) { + initializeWithArgs.invoke(null, (Object) Opt.targetArgs.toArray(new String[] {})); + } else if (initializeNoArgs != null) { + initializeNoArgs.invoke(null); + } + } catch (IllegalAccessException | InvocationTargetException e) { + err.print("== Java Exception in fuzzerInitialize: "); + e.printStackTrace(err); + exit(1); + } + + if (Opt.hooks) { + // libFuzzer will clear the coverage map after this method returns and keeps no record of the + // coverage accumulated so far (e.g. by static initializers). We record it here to keep it + // around for JaCoCo coverage reports. + CoverageRecorder.updateCoveredIdsWithCoverageMap(); + } + + Runtime.getRuntime().addShutdownHook(new Thread(FuzzTargetRunner::shutdown)); + } + + /** + * A test-only convenience wrapper around {@link #runOne(long, int)}. + */ + static int runOne(byte[] data) { + long dataPtr = UNSAFE.allocateMemory(data.length); + UNSAFE.copyMemory(data, BYTE_ARRAY_OFFSET, null, dataPtr, data.length); + try { + return runOne(dataPtr, data.length); + } finally { + UNSAFE.freeMemory(dataPtr); + } + } + + /** + * Executes the user-provided fuzz target once. + * + * @param dataPtr a native pointer to beginning of the input provided by the fuzzer for this + * execution + * @param dataLength length of the fuzzer input + * @return the value that the native LLVMFuzzerTestOneInput function should return. Currently, + * this is always 0. The function may exit the process instead of returning. + */ + private static int runOne(long dataPtr, int dataLength) { + Throwable finding = null; + byte[] data = null; + try { + if (useFuzzedDataProvider) { + fuzzedDataProvider.setNativeData(dataPtr, dataLength); + fuzzTarget.invokeExact((FuzzedDataProvider) fuzzedDataProvider); + } else { + data = copyToArray(dataPtr, dataLength); + fuzzTarget.invokeExact(data); + } + } catch (Throwable uncaughtFinding) { + finding = uncaughtFinding; + } + // Explicitly reported findings take precedence over uncaught exceptions. + if (JazzerInternal.lastFinding != null) { + finding = JazzerInternal.lastFinding; + JazzerInternal.lastFinding = null; + } + if (finding == null) { + return 0; + } + if (Opt.hooks) { + finding = ExceptionUtils.preprocessThrowable(finding); + } + + long dedupToken = Opt.dedup ? ExceptionUtils.computeDedupToken(finding) : 0; + // Opt.keepGoing implies Opt.dedup. + if (Opt.keepGoing > 1 && !ignoredTokens.add(dedupToken)) { + return 0; + } + + err.println(); + err.print("== Java Exception: "); + finding.printStackTrace(err); + if (Opt.dedup) { + // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does + // not necessarily have to appear at the beginning of a line. + // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342 + out.printf(Locale.ROOT, "DEDUP_TOKEN: %016x%n", dedupToken); + } + err.println("== libFuzzer crashing input =="); + printCrashingInput(); + // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it + // would report incorrect coverage - the reproducer generation involved rerunning the fuzz + // target. + dumpReproducer(data); + + if (Opt.keepGoing == 1 || Long.compareUnsigned(ignoredTokens.size(), Opt.keepGoing) >= 0) { + // Reached the maximum amount of findings to keep going for, crash after shutdown. We use + // _Exit rather than System.exit to not trigger libFuzzer's exit handlers. + shutdown(); + _Exit(LIBFUZZER_ERROR_EXIT_CODE); + throw new IllegalStateException("Not reached"); + } + return 0; + } + + /* + * Starts libFuzzer via LLVMFuzzerRunDriver. + * + * Note: Must be public rather than package-private as it is loaded in a different class loader + * than Driver. + */ + public static int startLibFuzzer(List<String> args) { + SignalHandler.initialize(); + return startLibFuzzer(Utils.toNativeArgs(args)); + } + + private static void shutdown() { + if (!Opt.coverageDump.isEmpty() || !Opt.coverageReport.isEmpty()) { + int[] everCoveredIds = CoverageMap.getEverCoveredIds(); + if (!Opt.coverageDump.isEmpty()) { + CoverageRecorder.dumpJacocoCoverage(everCoveredIds, Opt.coverageDump); + } + if (!Opt.coverageReport.isEmpty()) { + CoverageRecorder.dumpCoverageReport(everCoveredIds, Opt.coverageReport); + } + } + + Method teardown = targetPublicStaticMethodOrNull(FUZZER_TEARDOWN); + if (teardown == null) { + return; + } + err.println("calling fuzzerTearDown function"); + try { + teardown.invoke(null); + } catch (InvocationTargetException e) { + // An exception in fuzzerTearDown is a regular finding. + err.print("== Java Exception in fuzzerTearDown: "); + e.getCause().printStackTrace(err); + _Exit(LIBFUZZER_ERROR_EXIT_CODE); + } catch (Throwable t) { + // Any other exception is an error. + t.printStackTrace(err); + _Exit(1); + } + } + + private static String determineFuzzTargetClassName() { + if (!Opt.autofuzz.isEmpty()) { + return AUTOFUZZ_FUZZ_TARGET; + } + if (!Opt.targetClass.isEmpty()) { + return Opt.targetClass; + } + String manifestTargetClass = ManifestUtils.detectFuzzTargetClass(); + if (manifestTargetClass != null) { + return manifestTargetClass; + } + err.println("Missing argument --target_class=<fuzz_target_class>"); + exit(1); + throw new IllegalStateException("Not reached"); + } + + private static void dumpReproducer(byte[] data) { + if (data == null) { + assert useFuzzedDataProvider; + fuzzedDataProvider.reset(); + data = fuzzedDataProvider.consumeRemainingAsBytes(); + } + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 not available", e); + } + String dataSha1 = toHexString(digest.digest(data)); + + if (!Opt.autofuzz.isEmpty()) { + fuzzedDataProvider.reset(); + FuzzTarget.dumpReproducer(fuzzedDataProvider, Opt.reproducerPath, dataSha1); + return; + } + + String base64Data; + if (useFuzzedDataProvider) { + fuzzedDataProvider.reset(); + FuzzedDataProvider recordingFuzzedDataProvider = + RecordingFuzzedDataProvider.makeFuzzedDataProviderProxy(fuzzedDataProvider); + try { + fuzzTarget.invokeExact(recordingFuzzedDataProvider); + if (JazzerInternal.lastFinding == null) { + err.println("Failed to reproduce crash when rerunning with recorder"); + } + } catch (Throwable ignored) { + // Expected. + } + try { + base64Data = RecordingFuzzedDataProvider.serializeFuzzedDataProviderProxy( + recordingFuzzedDataProvider); + } catch (IOException e) { + err.print("ERROR: Failed to create reproducer: "); + e.printStackTrace(err); + // Don't let libFuzzer print a native stack trace. + _Exit(1); + throw new IllegalStateException("Not reached"); + } + } else { + base64Data = Base64.getEncoder().encodeToString(data); + } + + reproducerTemplate.dumpReproducer(base64Data, dataSha1); + } + + private static Method targetPublicStaticMethodOrNull(String name, Class<?>... parameterTypes) { + try { + Method method = fuzzTargetClass.getMethod(name, parameterTypes); + if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers())) { + return null; + } + return method; + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Convert a byte array to a lower-case hex string. + * + * <p>The returned hex string always has {@code 2 * bytes.length} characters. + * + * @param bytes the bytes to convert + * @return a lower-case hex string representing the bytes + */ + private static String toHexString(byte[] bytes) { + String unpadded = new BigInteger(1, bytes).toString(16); + int numLeadingZeroes = 2 * bytes.length - unpadded.length(); + return String.join("", Collections.nCopies(numLeadingZeroes, "0")) + unpadded; + } + + // Accessed by fuzz_target_runner.cpp. + @SuppressWarnings("unused") + private static void dumpAllStackTraces() { + ExceptionUtils.dumpAllStackTraces(); + } + + private static byte[] copyToArray(long ptr, int length) { + // TODO: Use Unsafe.allocateUninitializedArray instead once Java 9 is the base. + byte[] array = new byte[length]; + UNSAFE.copyMemory(null, ptr, array, BYTE_ARRAY_OFFSET, length); + return array; + } + + /** + * Starts libFuzzer via LLVMFuzzerRunDriver. + * + * @param args command-line arguments encoded in UTF-8 (not null-terminated) + * @return the return value of LLVMFuzzerRunDriver + */ + private static native int startLibFuzzer(byte[][] args); + + /** + * Causes libFuzzer to write the current input to disk as a crashing input and emit some + * information about it to stderr. + */ + private static native void printCrashingInput(); + + /** + * Immediately terminates the process without performing any cleanup. + * + * <p>Neither JVM shutdown hooks nor native exit handlers are called. This method does not return. + * + * <p>This method provides a way to exit Jazzer without triggering libFuzzer's exit hook that + * prints the "fuzz target exited" error message. It should thus be preferred over + * {@link System#exit} in any situation where Jazzer encounters an error after the fuzz target has + * started running. + * + * @param exitCode the exit code + */ + private static native void _Exit(int exitCode); +} diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java new file mode 100644 index 00000000..477c7d38 --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -0,0 +1,173 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import static java.lang.System.err; +import static java.lang.System.exit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Static options that determine the runtime behavior of the fuzzer, set via Java properties. + * + * <p>Each option corresponds to a command-line argument of the driver of the same name. + * + * <p>Every public field should be deeply immutable. + * + * <p>This class is loaded twice: As it is used in {@link FuzzTargetRunner}, it is loaded in the + * class loader that loads {@link Driver}. It is also used in + * {@link com.code_intelligence.jazzer.agent.Agent} after the agent JAR has been added to the + * bootstrap classpath and thus is loaded again in the bootstrap loader. This is not a problem since + * it only provides immutable fields and has no non-fatal side effects. + */ +public final class Opt { + private static final char SYSTEM_DELIMITER = + System.getProperty("os.name").startsWith("Windows") ? ';' : ':'; + + public static final String autofuzz = stringSetting("autofuzz", ""); + public static final List<String> autofuzzIgnore = stringListSetting("autofuzz_ignore", ','); + public static final String coverageDump = stringSetting("coverage_dump", ""); + public static final String coverageReport = stringSetting("coverage_report", ""); + public static final List<String> customHookIncludes = stringListSetting("custom_hook_includes"); + public static final List<String> customHookExcludes = stringListSetting("custom_hook_excludes"); + public static final List<String> customHooks = stringListSetting("custom_hooks"); + public static final List<String> disabledHooks = stringListSetting("disabled_hooks"); + public static final String dumpClassesDir = stringSetting("dump_classes_dir", ""); + public static final boolean hooks = boolSetting("hooks", true); + public static final String idSyncFile = stringSetting("id_sync_file", null); + public static final List<String> instrumentationIncludes = + stringListSetting("instrumentation_includes"); + public static final List<String> instrumentationExcludes = + stringListSetting("instrumentation_excludes"); + public static final Set<Long> ignore = + Collections.unmodifiableSet(stringListSetting("ignore", ',') + .stream() + .map(Long::parseUnsignedLong) + .collect(Collectors.toSet())); + public static final String reproducerPath = stringSetting("reproducer_path", "."); + public static final String targetClass = stringSetting("target_class", ""); + public static final List<String> trace = stringListSetting("trace"); + + // The values of these settings depend on autofuzz. + public static final List<String> targetArgs = autofuzz.isEmpty() + ? stringListSetting("target_args", ' ') + : Collections.unmodifiableList( + Stream.concat(Stream.of(autofuzz), autofuzzIgnore.stream()).collect(Collectors.toList())); + public static final long keepGoing = + uint64Setting("keep_going", autofuzz.isEmpty() ? 1 : Long.MAX_VALUE); + + // Default to false if hooks is false to mimic the original behavior of the native fuzz target + // runner, but still support hooks = false && dedup = true. + public static final boolean dedup = boolSetting("dedup", hooks); + + static { + if (!targetClass.isEmpty() && !autofuzz.isEmpty()) { + err.println("--target_class and --autofuzz cannot be specified together"); + exit(1); + } + if (!stringListSetting("target_args", ' ').isEmpty() && !autofuzz.isEmpty()) { + err.println("--target_args and --autofuzz cannot be specified together"); + exit(1); + } + if (autofuzz.isEmpty() && !autofuzzIgnore.isEmpty()) { + err.println("--autofuzz_ignore requires --autofuzz"); + exit(1); + } + if ((!ignore.isEmpty() || keepGoing > 1) && !dedup) { + // --autofuzz implicitly sets keepGoing to Integer.MAX_VALUE. + err.println("--nodedup is not supported with --ignore, --keep_going, or --autofuzz"); + exit(1); + } + } + + private static final String optionsPrefix = "jazzer."; + + private static String stringSetting(String name, String defaultValue) { + return System.getProperty(optionsPrefix + name, defaultValue); + } + + private static List<String> stringListSetting(String name) { + return stringListSetting(name, SYSTEM_DELIMITER); + } + + private static List<String> stringListSetting(String name, char separator) { + String value = System.getProperty(optionsPrefix + name); + if (value == null || value.isEmpty()) { + return Collections.emptyList(); + } + return splitOnUnescapedSeparator(value, separator); + } + + private static boolean boolSetting(String name, boolean defaultValue) { + String value = System.getProperty(optionsPrefix + name); + if (value == null) { + return defaultValue; + } + return Boolean.parseBoolean(value); + } + + private static long uint64Setting(String name, long defaultValue) { + String value = System.getProperty(optionsPrefix + name); + if (value == null) { + return defaultValue; + } + return Long.parseUnsignedLong(value, 10); + } + + /** + * Split value into non-empty takens separated by separator. Backslashes can be used to escape + * separators (or backslashes). + * + * @param value the string to split + * @param separator a single character to split on (backslash is not allowed) + * @return an immutable list of tokens obtained by splitting value on separator + */ + static List<String> splitOnUnescapedSeparator(String value, char separator) { + if (separator == '\\') { + throw new IllegalArgumentException("separator '\\' is not supported"); + } + ArrayList<String> tokens = new ArrayList<>(); + StringBuilder currentToken = new StringBuilder(); + boolean inEscapeState = false; + for (int pos = 0; pos < value.length(); pos++) { + char c = value.charAt(pos); + if (inEscapeState) { + currentToken.append(c); + inEscapeState = false; + } else if (c == '\\') { + inEscapeState = true; + } else if (c == separator) { + // Do not emit empty tokens between consecutive separators. + if (currentToken.length() > 0) { + tokens.add(currentToken.toString()); + } + currentToken.setLength(0); + } else { + currentToken.append(c); + } + } + if (currentToken.length() > 0) { + tokens.add(currentToken.toString()); + } + return Collections.unmodifiableList(tokens); + } +} diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl b/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl new file mode 100644 index 00000000..d9cb1e9e --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Reproducer.java.tmpl @@ -0,0 +1,28 @@ +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Crash_%1$s { + static final String base64Bytes = String.join("", "%2$s"); + + public static void main(String[] args) throws Throwable { + ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); + try { + Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize"); + fuzzerInitialize.invoke(null); + } catch (NoSuchMethodException ignored) { + try { + Method fuzzerInitialize = %3$s.class.getMethod("fuzzerInitialize", String[].class); + fuzzerInitialize.invoke(null, (Object) args); + } catch (NoSuchMethodException ignored1) { + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + System.exit(1); + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + System.exit(1); + } + %4$s + %3$s.fuzzerTestOneInput(input); + } +} diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java new file mode 100644 index 00000000..0c7721cf --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/ReproducerTemplate.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.stream.Collectors; + +final class ReproducerTemplate { + // A constant pool CONSTANT_Utf8_info entry should be able to hold data of size + // uint16, but somehow this does not seem to be the case and leads to invalid + // code crash reproducer code. Reducing the size by one resolves the problem. + private static final int DATA_CHUNK_MAX_LENGTH = Short.MAX_VALUE - 1; + private static final String RAW_BYTES_INPUT = + "byte[] input = java.util.Base64.getDecoder().decode(base64Bytes);"; + private static final String FUZZED_DATA_PROVIDER_INPUT = + "com.code_intelligence.jazzer.api.CannedFuzzedDataProvider input = new com.code_intelligence.jazzer.api.CannedFuzzedDataProvider(base64Bytes);"; + + private final String targetClass; + private final boolean useFuzzedDataProvider; + + public ReproducerTemplate(String targetClass, boolean useFuzzedDataProvider) { + this.targetClass = targetClass; + this.useFuzzedDataProvider = useFuzzedDataProvider; + } + + /** + * Emits a Java reproducer to {@code Crash_HASH.java} in {@code Opt.reproducerPath}. + * + * @param data the Base64-encoded data to emit as a string literal + * @param sha the SHA1 hash of the raw fuzzer input + */ + public void dumpReproducer(String data, String sha) { + String targetArg = useFuzzedDataProvider ? FUZZED_DATA_PROVIDER_INPUT : RAW_BYTES_INPUT; + String template = new BufferedReader( + new InputStreamReader(ReproducerTemplate.class.getResourceAsStream("Reproducer.java.tmpl"), + StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + String chunkedData = chunkStringLiteral(data); + String javaSource = String.format(template, sha, chunkedData, targetClass, targetArg); + Path javaPath = Paths.get(Opt.reproducerPath, String.format("Crash_%s.java", sha)); + try { + Files.write(javaPath, javaSource.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } catch (IOException e) { + System.err.printf("ERROR: Failed to write Java reproducer to %s%n", javaPath); + e.printStackTrace(); + } + System.out.printf( + "reproducer_path='%s'; Java reproducer written to %s%n", Opt.reproducerPath, javaPath); + } + + // The serialization of recorded FuzzedDataProvider invocations can get too long to be emitted + // into the template as a single String literal. This is mitigated by chunking the data and + // concatenating it again in the generated code. + private String chunkStringLiteral(String data) { + ArrayList<String> chunks = new ArrayList<>(); + for (int i = 0; i <= data.length() / DATA_CHUNK_MAX_LENGTH; i++) { + chunks.add(data.substring( + i * DATA_CHUNK_MAX_LENGTH, Math.min((i + 1) * DATA_CHUNK_MAX_LENGTH, data.length()))); + } + return String.join("\", \"", chunks); + } +} diff --git a/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java b/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java new file mode 100644 index 00000000..37eb1d0f --- /dev/null +++ b/driver/src/main/java/com/code_intelligence/jazzer/driver/Utils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class Utils { + /** + * Convert the arguments to UTF8 before passing them on to JNI as there are no JNI functions to + * get (unmodified) UTF-8 out of a jstring. + */ + static byte[][] toNativeArgs(Collection<String> args) { + return args.stream().map(str -> str.getBytes(StandardCharsets.UTF_8)).toArray(byte[][] ::new); + } + + static List<String> fromNativeArgs(byte[][] args) { + return Arrays.stream(args) + .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) + .collect(Collectors.toList()); + } +} diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel b/driver/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel new file mode 100644 index 00000000..863a1875 --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -0,0 +1,124 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") +load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") + +cc_jni_library( + name = "jazzer_driver", + visibility = [ + "//agent/src/jmh:__subpackages__", + "//agent/src/test:__subpackages__", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:__pkg__", + "//driver/src/test:__subpackages__", + ], + deps = [ + ":jazzer_driver_lib", + "@jazzer_libfuzzer//:libfuzzer_no_main", + ] + select({ + # Windows doesn't have a concept analogous to RTLD_GLOBAL. + "@platforms//os:windows": [], + "//conditions:default": [":trigger_driver_hooks_load"], + }), +) + +cc_library( + name = "jazzer_driver_lib", + visibility = ["//driver/src/test/native/com/code_intelligence/jazzer/driver/mocks:__pkg__"], + deps = [ + ":coverage_tracker", + ":fuzz_target_runner", + ":fuzzed_data_provider", + ":jazzer_fuzzer_callbacks", + ":libfuzzer_callbacks", + ], +) + +cc_library( + name = "coverage_tracker", + srcs = ["coverage_tracker.cpp"], + hdrs = ["coverage_tracker.h"], + deps = ["//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs"], + # Symbols are only referenced dynamically via JNI. + alwayslink = True, +) + +cc_library( + name = "fuzz_target_runner", + srcs = ["fuzz_target_runner.cpp"], + hdrs = ["fuzz_target_runner.h"], + linkopts = select({ + "@platforms//os:windows": [], + "//conditions:default": ["-ldl"], + }), + deps = [ + ":sanitizer_symbols", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner.hdrs", + ], + # With sanitizers, symbols are only referenced dynamically via JNI. + alwayslink = True, +) + +cc_library( + name = "fuzzed_data_provider", + srcs = ["fuzzed_data_provider.cpp"], + visibility = [ + "//driver:__pkg__", + ], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:fuzzed_data_provider.hdrs", + ], + # Symbols may only be referenced dynamically via JNI. + alwayslink = True, +) + +cc_jni_library( + name = "fuzzed_data_provider_standalone", + visibility = ["//agent/src/main/java/com/code_intelligence/jazzer/replay:__pkg__"], + deps = [":fuzzed_data_provider"], +) + +cc_library( + name = "jazzer_fuzzer_callbacks", + srcs = ["jazzer_fuzzer_callbacks.cpp"], + deps = [ + ":sanitizer_hooks_with_pc", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:trace_data_flow_native_callbacks.hdrs", + ], + alwayslink = True, +) + +cc_library( + name = "libfuzzer_callbacks", + srcs = ["libfuzzer_callbacks.cpp"], + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:trace_data_flow_native_callbacks.hdrs", + "@com_google_absl//absl/strings", + ], + # Symbols are only referenced dynamically via JNI. + alwayslink = True, +) + +cc_library( + name = "trigger_driver_hooks_load", + srcs = ["trigger_driver_hooks_load.cpp"], + linkopts = ["-ldl"], + target_compatible_with = SKIP_ON_WINDOWS, + deps = ["@fmeum_rules_jni//jni"], + # Symbols are only referenced dynamically via JNI. + alwayslink = True, +) + +cc_library( + name = "sanitizer_hooks_with_pc", + hdrs = ["sanitizer_hooks_with_pc.h"], + visibility = [ + "//agent/src/jmh/native:__subpackages__", + "//driver:__pkg__", + "//driver/src/test/native/com/code_intelligence/jazzer/driver:__pkg__", + ], +) + +cc_library( + name = "sanitizer_symbols", + srcs = ["sanitizer_symbols.cpp"], + # Symbols are referenced dynamically by libFuzzer. + alwayslink = True, +) diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp new file mode 100644 index 00000000..dc8349d4 --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp @@ -0,0 +1,114 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "coverage_tracker.h" + +#include <jni.h> + +#include <stdexcept> +#include <vector> + +#include "com_code_intelligence_jazzer_runtime_CoverageMap.h" + +extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start, + uint8_t *end); +extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, + const uintptr_t *pcs_end); +extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries); + +namespace { +void AssertNoException(JNIEnv &env) { + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + throw std::runtime_error( + "Java exception occurred in CoverageTracker JNI code"); + } +} +} // namespace + +namespace jazzer { + +uint8_t *CoverageTracker::counters_ = nullptr; +PCTableEntry *CoverageTracker::pc_entries_ = nullptr; + +void CoverageTracker::Initialize(JNIEnv &env, jlong counters) { + if (counters_ != nullptr) { + throw std::runtime_error( + "CoverageTracker::Initialize must not be called more than once"); + } + counters_ = reinterpret_cast<uint8_t *>(static_cast<uintptr_t>(counters)); +} + +void CoverageTracker::RegisterNewCounters(JNIEnv &env, jint old_num_counters, + jint new_num_counters) { + if (counters_ == nullptr) { + throw std::runtime_error( + "CoverageTracker::Initialize should have been called first"); + } + if (new_num_counters < old_num_counters) { + throw std::runtime_error( + "new_num_counters must not be smaller than old_num_counters"); + } + if (new_num_counters == old_num_counters) { + return; + } + std::size_t diff_num_counters = new_num_counters - old_num_counters; + // libFuzzer requires an array containing the instruction addresses associated + // with the coverage counters registered above. This is required to report how + // many edges have been covered. However, libFuzzer only checks these + // addresses when the corresponding flag is set to 1. Therefore, it is safe to + // set the all PC entries to any value as long as the corresponding flag is + // set to zero. We set the value of each PC to the index of the corresponding + // edge ID. This facilitates finding the edge ID of each covered PC reported + // by libFuzzer. + pc_entries_ = new PCTableEntry[diff_num_counters]; + for (std::size_t i = 0; i < diff_num_counters; ++i) { + pc_entries_[i] = {i, 0}; + } + __sanitizer_cov_8bit_counters_init(counters_ + old_num_counters, + counters_ + new_num_counters); + __sanitizer_cov_pcs_init((uintptr_t *)(pc_entries_), + (uintptr_t *)(pc_entries_ + diff_num_counters)); +} +} // namespace jazzer + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_initialize( + JNIEnv *env, jclass, jlong counters) { + ::jazzer::CoverageTracker::Initialize(*env, counters); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_registerNewCounters( + JNIEnv *env, jclass, jint old_num_counters, jint new_num_counters) { + ::jazzer::CoverageTracker::RegisterNewCounters(*env, old_num_counters, + new_num_counters); +} + +[[maybe_unused]] jintArray +Java_com_code_1intelligence_jazzer_runtime_CoverageMap_getEverCoveredIds( + JNIEnv *env, jclass) { + uintptr_t *covered_pcs; + jint num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs); + std::vector<jint> covered_edge_ids(covered_pcs, + covered_pcs + num_covered_pcs); + delete[] covered_pcs; + + jintArray covered_edge_ids_jni = env->NewIntArray(num_covered_pcs); + AssertNoException(*env); + env->SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs, + covered_edge_ids.data()); + AssertNoException(*env); + return covered_edge_ids_jni; +} diff --git a/driver/coverage_tracker.h b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h index 5b237de3..8cceceed 100644 --- a/driver/coverage_tracker.h +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h @@ -20,8 +20,6 @@ #include <string> -#include "jvm_tooling.h" - namespace jazzer { // The members of this struct are only accessed by libFuzzer. @@ -30,29 +28,15 @@ struct __attribute__((packed)) PCTableEntry { }; // CoverageTracker registers an array of 8-bit coverage counters with -// libFuzzer. The array is backed by a MappedByteBuffer on the Java -// side, where it is populated with the actual coverage information. -class CoverageTracker : public ExceptionPrinter { +// libFuzzer. The array is populated from Java using Unsafe. +class CoverageTracker { private: static uint8_t *counters_; - - static uint32_t *fake_instructions_; static PCTableEntry *pc_entries_; - static void JNICALL RegisterNewCoverageCounters(JNIEnv &env, jclass cls); - public: - static void Setup(JNIEnv &env); - // Clears the coverage counters array manually. It is cleared automatically - // by libFuzzer prior to running the fuzz target, so this function is only - // used in tests. - static void Clear(); - - // Returns the address of the coverage counters array. - static uint8_t *GetCoverageCounters(); - - static void RecordInitialCoverage(JNIEnv &env); - static void ReplayInitialCoverage(JNIEnv &env); - static std::string ComputeCoverage(JNIEnv &env); + static void Initialize(JNIEnv &env, jlong counters); + static void RegisterNewCounters(JNIEnv &env, jint old_num_counters, + jint new_num_counters); }; } // namespace jazzer diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp new file mode 100644 index 00000000..6231af09 --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.cpp @@ -0,0 +1,176 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * A native wrapper around the FuzzTargetRunner Java class that executes it as a + * libFuzzer fuzz target. + */ + +#include "fuzz_target_runner.h" + +#ifndef _WIN32 +#include <dlfcn.h> +#endif +#include <jni.h> + +#include <iostream> +#include <limits> +#include <string> +#include <vector> + +#include "com_code_intelligence_jazzer_driver_FuzzTargetRunner.h" + +extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv, + int (*UserCb)(const uint8_t *Data, + size_t Size)); + +namespace { +jclass gRunner; +jmethodID gRunOneId; +JavaVM *gJavaVm; +JNIEnv *gEnv; + +// A libFuzzer-registered callback that outputs the crashing input, but does +// not include a stack trace. +void (*gLibfuzzerPrintCrashingInput)() = nullptr; + +int testOneInput(const uint8_t *data, const std::size_t size) { + JNIEnv &env = *gEnv; + jint jsize = + std::min(size, static_cast<size_t>(std::numeric_limits<jint>::max())); + int res = env.CallStaticIntMethod(gRunner, gRunOneId, data, jsize); + if (env.ExceptionCheck()) { + env.ExceptionDescribe(); + _Exit(1); + } + return res; +} +} // namespace + +namespace jazzer { +void DumpJvmStackTraces() { + JNIEnv *env = nullptr; + if (gJavaVm->AttachCurrentThread(reinterpret_cast<void **>(&env), nullptr) != + JNI_OK) { + std::cerr << "WARN: AttachCurrentThread failed in DumpJvmStackTraces" + << std::endl; + return; + } + jmethodID dumpStack = + env->GetStaticMethodID(gRunner, "dumpAllStackTraces", "()V"); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return; + } + env->CallStaticVoidMethod(gRunner, dumpStack); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return; + } + // Do not detach as we may be the main thread (but the JVM exits anyway). +} +} // namespace jazzer + +[[maybe_unused]] jint +Java_com_code_1intelligence_jazzer_driver_FuzzTargetRunner_startLibFuzzer( + JNIEnv *env, jclass runner, jobjectArray args) { + gEnv = env; + env->GetJavaVM(&gJavaVm); + gRunner = reinterpret_cast<jclass>(env->NewGlobalRef(runner)); + gRunOneId = env->GetStaticMethodID(runner, "runOne", "(JI)I"); + if (gRunOneId == nullptr) { + env->ExceptionDescribe(); + _Exit(1); + } + + int argc = env->GetArrayLength(args); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + _Exit(1); + } + std::vector<std::string> argv_strings; + std::vector<const char *> argv_c; + for (jsize i = 0; i < argc; i++) { + auto arg_jni = + reinterpret_cast<jbyteArray>(env->GetObjectArrayElement(args, i)); + if (arg_jni == nullptr) { + env->ExceptionDescribe(); + _Exit(1); + } + jbyte *arg_c = env->GetByteArrayElements(arg_jni, nullptr); + if (arg_c == nullptr) { + env->ExceptionDescribe(); + _Exit(1); + } + std::size_t arg_size = env->GetArrayLength(arg_jni); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + _Exit(1); + } + argv_strings.emplace_back(reinterpret_cast<const char *>(arg_c), arg_size); + env->ReleaseByteArrayElements(arg_jni, arg_c, JNI_ABORT); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + _Exit(1); + } + } + for (jsize i = 0; i < argc; i++) { + argv_c.emplace_back(argv_strings[i].c_str()); + } + // Null-terminate argv. + argv_c.emplace_back(nullptr); + + const char **argv = argv_c.data(); + return LLVMFuzzerRunDriver(&argc, const_cast<char ***>(&argv), testOneInput); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_driver_FuzzTargetRunner_printCrashingInput( + JNIEnv *, jclass) { + if (gLibfuzzerPrintCrashingInput == nullptr) { + std::cerr << "<not available>" << std::endl; + } else { + gLibfuzzerPrintCrashingInput(); + } +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_driver_FuzzTargetRunner__1Exit( + JNIEnv *, jclass, jint exit_code) { + _Exit(exit_code); +} + +// We apply a patch to libFuzzer to make it call this function instead of +// __sanitizer_set_death_callback to pass us the death callback. +extern "C" [[maybe_unused]] void __jazzer_set_death_callback( + void (*callback)()) { + gLibfuzzerPrintCrashingInput = callback; +#ifndef _WIN32 + void *sanitizer_set_death_callback = + dlsym(RTLD_DEFAULT, "__sanitizer_set_death_callback"); + if (sanitizer_set_death_callback != nullptr) { + (reinterpret_cast<void (*)(void (*)())>(sanitizer_set_death_callback))( + []() { + ::jazzer::DumpJvmStackTraces(); + gLibfuzzerPrintCrashingInput(); + // Ideally, we would be able to perform a graceful shutdown of the + // JVM. However, doing this directly results in a nested bug report by + // ASan or UBSan, likely because something about the stack/thread + // context in which they generate reports is incompatible with the JVM + // shutdown process. use_sigaltstack=0 does not help though, so this + // might be on us. + }); + } +#endif +} diff --git a/driver/libfuzzer_callbacks.h b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.h index 985809a7..0e8846c0 100644 --- a/driver/libfuzzer_callbacks.h +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.h @@ -19,7 +19,10 @@ #include <jni.h> namespace jazzer { - -bool registerFuzzerCallbacks(JNIEnv &env); - +/* + * Print the stack traces of all active JVM threads. + * + * This function can be called from any thread. + */ +void DumpJvmStackTraces(); } // namespace jazzer diff --git a/driver/fuzzed_data_provider.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzzed_data_provider.cpp index e8cb971b..494bb9e8 100644 --- a/driver/fuzzed_data_provider.cpp +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/fuzzed_data_provider.cpp @@ -43,31 +43,19 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // -#include "fuzzed_data_provider.h" - #include <algorithm> #include <cstdint> +#include <limits> #include <string> +#include <tuple> #include <type_traits> -#include <vector> -#include "absl/strings/str_format.h" +#include "com_code_intelligence_jazzer_runtime_FuzzedDataProviderImpl.h" namespace { -const uint8_t *gDataPtr = nullptr; -std::size_t gRemainingBytes = 0; - -// Advance by `bytes` bytes in the buffer or stay at the end if it has been -// consumed. -void Advance(const std::size_t bytes) { - if (bytes > gRemainingBytes) { - gRemainingBytes = 0; - } else { - gDataPtr += bytes; - gRemainingBytes -= bytes; - } -} +jfieldID gDataPtrField = nullptr; +jfieldID gRemainingBytesField = nullptr; void ThrowIllegalArgumentException(JNIEnv &env, const std::string &message) { jclass illegal_argument_exception = @@ -105,13 +93,22 @@ ConsumeIntegralArray(JNIEnv &env, jobject self, jint max_length) { } // Arrays of integral types are considered data and thus consumed from the // beginning of the buffer. - std::size_t max_num_bytes = std::min(sizeof(T) * max_length, gRemainingBytes); + const auto *dataPtr = + reinterpret_cast<const uint8_t *>(env.GetLongField(self, gDataPtrField)); + jint remainingBytes = env.GetIntField(self, gRemainingBytesField); + + jint max_num_bytes = + std::min(static_cast<jint>(sizeof(T)) * max_length, remainingBytes); jsize actual_length = max_num_bytes / sizeof(T); - std::size_t actual_num_bytes = sizeof(T) * actual_length; + jint actual_num_bytes = sizeof(T) * actual_length; auto array = (env.*(JniArrayType<T>::kNewArrayFunc))(actual_length); (env.*(JniArrayType<T>::kSetArrayRegionFunc))( - array, 0, actual_length, reinterpret_cast<const T *>(gDataPtr)); - Advance(actual_num_bytes); + array, 0, actual_length, reinterpret_cast<const T *>(dataPtr)); + + env.SetLongField(self, gDataPtrField, (jlong)(dataPtr + actual_num_bytes)); + env.SetIntField(self, gRemainingBytesField, + remainingBytes - actual_num_bytes); + return array; } @@ -122,25 +119,24 @@ jbyteArray JNICALL ConsumeRemainingAsArray(JNIEnv &env, jobject self) { template <typename T> T JNICALL ConsumeIntegralInRange(JNIEnv &env, jobject self, T min, T max) { - if (min > max) { - ThrowIllegalArgumentException( - env, absl::StrFormat( - "Consume*InRange: min must be <= max (got min: %d, max: %d)", - min, max)); - return 0; - } - uint64_t range = static_cast<uint64_t>(max) - min; uint64_t result = 0; - std::size_t offset = 0; + jint offset = 0; + + const auto *dataPtr = + reinterpret_cast<const uint8_t *>(env.GetLongField(self, gDataPtrField)); + jint remainingBytes = env.GetIntField(self, gRemainingBytesField); while (offset < 8 * sizeof(T) && (range >> offset) > 0 && - gRemainingBytes != 0) { - --gRemainingBytes; - result = (result << 8u) | gDataPtr[gRemainingBytes]; + remainingBytes != 0) { + --remainingBytes; + result = (result << 8u) | dataPtr[remainingBytes]; offset += 8; } + env.SetIntField(self, gRemainingBytesField, remainingBytes); + // dataPtr hasn't been modified, so we don't need to update gDataPtrField. + if (range != std::numeric_limits<T>::max()) // We accept modulo bias in favor of reading a dynamic number of bytes as // this would make it harder for the fuzzer to mutate towards values from @@ -197,14 +193,6 @@ T JNICALL ConsumeProbability(JNIEnv &env, jobject self) { template <typename T> T JNICALL ConsumeFloatInRange(JNIEnv &env, jobject self, T min, T max) { - if (min > max) { - ThrowIllegalArgumentException( - env, absl::StrFormat( - "Consume*InRange: min must be <= max (got min: %f, max: %f)", - min, max)); - return 0.0; - } - T range; T result = min; @@ -230,7 +218,7 @@ T JNICALL ConsumeRegularFloat(JNIEnv &env, jobject self) { template <typename T> T JNICALL ConsumeFloat(JNIEnv &env, jobject self) { - if (!gRemainingBytes) return 0.0; + if (env.GetIntField(self, gRemainingBytesField) == 0) return 0.0; auto type_val = ConsumeIntegral<uint8_t>(env, self); @@ -332,27 +320,25 @@ enum class Utf8GenerationState { // See Algorithm 1 of https://arxiv.org/pdf/2010.03090.pdf for more details on // the individual cases involved in determining the validity of a UTF-8 string. template <bool ascii_only, bool stop_on_backslash> -std::pair<std::string, std::size_t> FixUpModifiedUtf8(const uint8_t *data, - std::size_t max_bytes, - jint max_length) { +std::pair<std::string, jint> FixUpModifiedUtf8(const uint8_t *data, + jint max_bytes, + jint max_length) { std::string str; // Every character in modified UTF-8 is coded on at most six bytes. Every // consumed byte is transformed into at most one code unit, except for the // case of a zero byte which requires two bytes. - if (max_bytes > std::numeric_limits<std::size_t>::max() / 2) - max_bytes = std::numeric_limits<std::size_t>::max() / 2; if (ascii_only) { - str.reserve( - std::min(2 * static_cast<std::size_t>(max_length), 2 * max_bytes)); + str.reserve(std::min(2 * static_cast<std::size_t>(max_length), + 2 * static_cast<std::size_t>(max_bytes))); } else { - str.reserve( - std::min(6 * static_cast<std::size_t>(max_length), 2 * max_bytes)); + str.reserve(std::min(6 * static_cast<std::size_t>(max_length), + 2 * static_cast<std::size_t>(max_bytes))); } Utf8GenerationState state = Utf8GenerationState::LeadingByte_Generic; const uint8_t *pos = data; const auto data_end = data + max_bytes; - for (std::size_t length = 0; length < max_length && pos != data_end; ++pos) { + for (jint length = 0; length < max_length && pos != data_end; ++pos) { uint8_t c = *pos; if (ascii_only) { // Clamp to 7-bit ASCII range. @@ -568,11 +554,10 @@ done: namespace jazzer { // Exposed for testing only. -std::pair<std::string, std::size_t> FixUpModifiedUtf8(const uint8_t *data, - std::size_t max_bytes, - jint max_length, - bool ascii_only, - bool stop_on_backslash) { +std::pair<std::string, jint> FixUpModifiedUtf8(const uint8_t *data, + jint max_bytes, jint max_length, + bool ascii_only, + bool stop_on_backslash) { if (ascii_only) { if (stop_on_backslash) { return ::FixUpModifiedUtf8<true, true>(data, max_bytes, max_length); @@ -590,81 +575,85 @@ std::pair<std::string, std::size_t> FixUpModifiedUtf8(const uint8_t *data, } // namespace jazzer namespace { -jstring ConsumeStringInternal(JNIEnv &env, jint max_length, bool ascii_only, - bool stop_on_backslash) { +jstring ConsumeStringInternal(JNIEnv &env, jobject self, jint max_length, + bool ascii_only, bool stop_on_backslash) { if (max_length < 0) { ThrowIllegalArgumentException(env, "maxLength must not be negative"); return nullptr; } - if (max_length == 0 || gRemainingBytes == 0) return env.NewStringUTF(""); + const auto *dataPtr = + reinterpret_cast<const uint8_t *>(env.GetLongField(self, gDataPtrField)); + jint remainingBytes = env.GetIntField(self, gRemainingBytesField); - if (gRemainingBytes == 1) { - Advance(1); + if (max_length == 0 || remainingBytes == 0) return env.NewStringUTF(""); + + if (remainingBytes == 1) { + env.SetIntField(self, gRemainingBytesField, 0); return env.NewStringUTF(""); } - std::size_t max_bytes = gRemainingBytes; std::string str; - std::size_t consumed_bytes; + jint consumed_bytes; std::tie(str, consumed_bytes) = jazzer::FixUpModifiedUtf8( - gDataPtr, max_bytes, max_length, ascii_only, stop_on_backslash); - Advance(consumed_bytes); + dataPtr, remainingBytes, max_length, ascii_only, stop_on_backslash); + env.SetLongField(self, gDataPtrField, (jlong)(dataPtr + consumed_bytes)); + env.SetIntField(self, gRemainingBytesField, remainingBytes - consumed_bytes); return env.NewStringUTF(str.c_str()); } jstring JNICALL ConsumeAsciiString(JNIEnv &env, jobject self, jint max_length) { - return ConsumeStringInternal(env, max_length, true, true); + return ConsumeStringInternal(env, self, max_length, true, true); } jstring JNICALL ConsumeString(JNIEnv &env, jobject self, jint max_length) { - return ConsumeStringInternal(env, max_length, false, true); + return ConsumeStringInternal(env, self, max_length, false, true); } jstring JNICALL ConsumeRemainingAsAsciiString(JNIEnv &env, jobject self) { - return ConsumeStringInternal(env, std::numeric_limits<jint>::max(), true, - false); + return ConsumeStringInternal(env, self, std::numeric_limits<jint>::max(), + true, false); } jstring JNICALL ConsumeRemainingAsString(JNIEnv &env, jobject self) { - return ConsumeStringInternal(env, std::numeric_limits<jint>::max(), false, - false); + return ConsumeStringInternal(env, self, std::numeric_limits<jint>::max(), + false, false); } std::size_t RemainingBytes(JNIEnv &env, jobject self) { - return gRemainingBytes; + return env.GetIntField(self, gRemainingBytesField); } const JNINativeMethod kFuzzedDataMethods[]{ {(char *)"consumeBoolean", (char *)"()Z", (void *)&ConsumeBool}, {(char *)"consumeByte", (char *)"()B", (void *)&ConsumeIntegral<jbyte>}, - {(char *)"consumeByte", (char *)"(BB)B", + {(char *)"consumeByteUnchecked", (char *)"(BB)B", (void *)&ConsumeIntegralInRange<jbyte>}, {(char *)"consumeShort", (char *)"()S", (void *)&ConsumeIntegral<jshort>}, - {(char *)"consumeShort", (char *)"(SS)S", + {(char *)"consumeShortUnchecked", (char *)"(SS)S", (void *)&ConsumeIntegralInRange<jshort>}, {(char *)"consumeInt", (char *)"()I", (void *)&ConsumeIntegral<jint>}, - {(char *)"consumeInt", (char *)"(II)I", + {(char *)"consumeIntUnchecked", (char *)"(II)I", (void *)&ConsumeIntegralInRange<jint>}, {(char *)"consumeLong", (char *)"()J", (void *)&ConsumeIntegral<jlong>}, - {(char *)"consumeLong", (char *)"(JJ)J", + {(char *)"consumeLongUnchecked", (char *)"(JJ)J", (void *)&ConsumeIntegralInRange<jlong>}, {(char *)"consumeFloat", (char *)"()F", (void *)&ConsumeFloat<jfloat>}, {(char *)"consumeRegularFloat", (char *)"()F", (void *)&ConsumeRegularFloat<jfloat>}, - {(char *)"consumeRegularFloat", (char *)"(FF)F", + {(char *)"consumeRegularFloatUnchecked", (char *)"(FF)F", (void *)&ConsumeFloatInRange<jfloat>}, {(char *)"consumeProbabilityFloat", (char *)"()F", (void *)&ConsumeProbability<jfloat>}, {(char *)"consumeDouble", (char *)"()D", (void *)&ConsumeFloat<jdouble>}, {(char *)"consumeRegularDouble", (char *)"()D", (void *)&ConsumeRegularFloat<jdouble>}, - {(char *)"consumeRegularDouble", (char *)"(DD)D", + {(char *)"consumeRegularDoubleUnchecked", (char *)"(DD)D", (void *)&ConsumeFloatInRange<jdouble>}, {(char *)"consumeProbabilityDouble", (char *)"()D", (void *)&ConsumeProbability<jdouble>}, {(char *)"consumeChar", (char *)"()C", (void *)&ConsumeChar}, - {(char *)"consumeChar", (char *)"(CC)C", + {(char *)"consumeCharUnchecked", (char *)"(CC)C", (void *)&ConsumeIntegralInRange<jchar>}, {(char *)"consumeCharNoSurrogates", (char *)"()C", (void *)&ConsumeCharNoSurrogates}, @@ -694,26 +683,10 @@ const jint kNumFuzzedDataMethods = sizeof(kFuzzedDataMethods) / sizeof(kFuzzedDataMethods[0]); } // namespace -namespace jazzer { - -void SetUpFuzzedDataProvider(JNIEnv &env) { - jclass fuzzed_data_provider_class = - env.FindClass(kFuzzedDataProviderImplClass); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error("failed to find FuzzedDataProviderImpl class"); - } - env.RegisterNatives(fuzzed_data_provider_class, kFuzzedDataMethods, - kNumFuzzedDataMethods); - if (env.ExceptionCheck()) { - env.ExceptionDescribe(); - throw std::runtime_error( - "could not register native callbacks for FuzzedDataProvider"); - } +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_FuzzedDataProviderImpl_nativeInit( + JNIEnv *env, jclass clazz) { + env->RegisterNatives(clazz, kFuzzedDataMethods, kNumFuzzedDataMethods); + gDataPtrField = env->GetFieldID(clazz, "dataPtr", "J"); + gRemainingBytesField = env->GetFieldID(clazz, "remainingBytes", "I"); } - -void FeedFuzzedDataProvider(const uint8_t *data, std::size_t size) { - gDataPtr = data; - gRemainingBytes = size; -} -} // namespace jazzer diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/jazzer_fuzzer_callbacks.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/jazzer_fuzzer_callbacks.cpp new file mode 100644 index 00000000..8764aaaa --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/jazzer_fuzzer_callbacks.cpp @@ -0,0 +1,184 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <jni.h> + +#include <cstddef> +#include <cstdint> + +#include "com_code_intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks.h" +#include "sanitizer_hooks_with_pc.h" + +namespace { + +extern "C" { +void __sanitizer_weak_hook_compare_bytes(void *caller_pc, const void *s1, + const void *s2, std::size_t n1, + std::size_t n2, int result); +void __sanitizer_weak_hook_memmem(void *called_pc, const void *s1, size_t len1, + const void *s2, size_t len2, void *result); +} + +inline __attribute__((always_inline)) void *idToPc(jint id) { + return reinterpret_cast<void *>(static_cast<uintptr_t>(id)); +} +} // namespace + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceStrstr0( + JNIEnv *env, jclass cls, jbyteArray needle, jint id) { + jint needle_length = env->GetArrayLength(needle); + auto *needle_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(needle, nullptr)); + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle_native, + needle_length, nullptr); + env->ReleasePrimitiveArrayCritical(needle, needle_native, JNI_ABORT); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceStrstr0( + jint needle_length, jbyte *needle_native, jint id) { + __sanitizer_weak_hook_memmem(idToPc(id), nullptr, 0, needle_native, + needle_length, nullptr); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceMemcmp( + JNIEnv *env, jclass cls, jbyteArray b1, jbyteArray b2, jint result, + jint id) { + jint b1_length = env->GetArrayLength(b1); + jint b2_length = env->GetArrayLength(b2); + auto *b1_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b1, nullptr)); + auto *b2_native = + static_cast<jbyte *>(env->GetPrimitiveArrayCritical(b2, nullptr)); + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1_native, b2_native, + b1_length, b2_length, result); + env->ReleasePrimitiveArrayCritical(b1, b1_native, JNI_ABORT); + env->ReleasePrimitiveArrayCritical(b2, b2_native, JNI_ABORT); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceMemcmp( + jint b1_length, jbyte *b1, jint b2_length, jbyte *b2, jint result, + jint id) { + __sanitizer_weak_hook_compare_bytes(idToPc(id), b1, b2, b1_length, b2_length, + result); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceCmpLong( + JNIEnv *env, jclass cls, jlong value1, jlong value2, jint id) { + __sanitizer_cov_trace_cmp8_with_pc(idToPc(id), value1, value2); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceCmpLong( + jlong value1, jlong value2, jint id) { + __sanitizer_cov_trace_cmp8_with_pc(idToPc(id), value1, value2); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceCmpInt( + jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceConstCmpInt( + JNIEnv *env, jclass cls, jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceConstCmpInt( + jint value1, jint value2, jint id) { + __sanitizer_cov_trace_cmp4_with_pc(idToPc(id), value1, value2); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceSwitch( + JNIEnv *env, jclass cls, jlong switch_value, + jlongArray libfuzzer_case_values, jint id) { + auto *case_values = static_cast<jlong *>( + env->GetPrimitiveArrayCritical(libfuzzer_case_values, nullptr)); + __sanitizer_cov_trace_switch_with_pc( + idToPc(id), switch_value, reinterpret_cast<uint64_t *>(case_values)); + env->ReleasePrimitiveArrayCritical(libfuzzer_case_values, case_values, + JNI_ABORT); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceSwitch( + jlong switch_value, jint libfuzzer_case_values_length, jlong *case_values, + jint id) { + __sanitizer_cov_trace_switch_with_pc( + idToPc(id), switch_value, reinterpret_cast<uint64_t *>(case_values)); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceDivLong( + JNIEnv *env, jclass cls, jlong value, jint id) { + __sanitizer_cov_trace_div8_with_pc(idToPc(id), value); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceDivLong( + jlong value, jint id) { + __sanitizer_cov_trace_div8_with_pc(idToPc(id), value); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceDivInt( + JNIEnv *env, jclass cls, jint value, jint id) { + __sanitizer_cov_trace_div4_with_pc(idToPc(id), value); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceDivInt( + jint value, jint id) { + __sanitizer_cov_trace_div4_with_pc(idToPc(id), value); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceGep( + JNIEnv *env, jclass cls, jlong idx, jint id) { + __sanitizer_cov_trace_gep_with_pc(idToPc(id), static_cast<uintptr_t>(idx)); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_traceGep( + jlong idx, jint id) { + __sanitizer_cov_trace_gep_with_pc(idToPc(id), static_cast<uintptr_t>(idx)); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_tracePcIndir( + JNIEnv *env, jclass cls, jint caller_id, jint callee_id) { + __sanitizer_cov_trace_pc_indir_with_pc(idToPc(caller_id), + static_cast<uintptr_t>(callee_id)); +} + +extern "C" [[maybe_unused]] JNIEXPORT void JNICALL +JavaCritical_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_tracePcIndir( + jint caller_id, jint callee_id) { + __sanitizer_cov_trace_pc_indir_with_pc(idToPc(caller_id), + static_cast<uintptr_t>(callee_id)); +} diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/libfuzzer_callbacks.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/libfuzzer_callbacks.cpp new file mode 100644 index 00000000..a20863fa --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/libfuzzer_callbacks.cpp @@ -0,0 +1,129 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <jni.h> + +#include <algorithm> +#include <fstream> +#include <iostream> +#include <mutex> +#include <utility> +#include <vector> + +#include "absl/strings/str_split.h" +#include "com_code_intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks.h" + +namespace { +bool is_using_native_libraries = false; +std::once_flag ignore_list_flag; +std::vector<std::pair<uintptr_t, uintptr_t>> ignore_for_interception_ranges; + +/** + * Adds the address ranges of executable segmentes of the library lib_name to + * the ignorelist for C standard library function interception (strcmp, memcmp, + * ...). + */ +void ignoreLibraryForInterception(const std::string &lib_name) { + std::ifstream loaded_libs("/proc/self/maps"); + if (!loaded_libs) { + // This early exit is taken e.g. on macOS, where /proc does not exist. + return; + } + std::string line; + while (std::getline(loaded_libs, line)) { + if (!absl::StrContains(line, lib_name)) continue; + // clang-format off + // A typical line looks as follows: + // 7f15356c9000-7f1536367000 r-xp 0020d000 fd:01 19275673 /usr/lib/jvm/java-15-openjdk-amd64/lib/server/libjvm.so + // clang-format on + std::vector<std::string> parts = + absl::StrSplit(line, ' ', absl::SkipEmpty()); + if (parts.size() != 6) { + std::cout << "ERROR: Invalid format for /proc/self/maps\n" + << line << std::endl; + exit(1); + } + // Skip non-executable address rang"s. + if (!absl::StrContains(parts[1], "x")) continue; + std::string range_str = parts[0]; + std::vector<std::string> range = absl::StrSplit(range_str, "-"); + if (range.size() != 2) { + std::cout + << "ERROR: Unexpected address range format in /proc/self/maps line: " + << range_str << std::endl; + exit(1); + } + std::size_t pos; + auto start = std::stoull(range[0], &pos, 16); + if (pos != range[0].size()) { + std::cout + << "ERROR: Unexpected address range format in /proc/self/maps line: " + << range_str << std::endl; + exit(1); + } + auto end = std::stoull(range[1], &pos, 16); + if (pos != range[0].size()) { + std::cout + << "ERROR: Unexpected address range format in /proc/self/maps line: " + << range_str << std::endl; + exit(1); + } + ignore_for_interception_ranges.emplace_back(start, end); + } +} + +const std::vector<std::string> kLibrariesToIgnoreForInterception = { + // The driver executable itself can be treated just like a library. + "jazzer_driver", "libinstrument.so", "libjava.so", + "libjimage.so", "libjli.so", "libjvm.so", + "libnet.so", "libverify.so", "libzip.so", +}; +} // namespace + +extern "C" [[maybe_unused]] bool __sanitizer_weak_is_relevant_pc( + void *caller_pc) { + // If the fuzz target is not using native libraries, calls to strcmp, memcmp, + // etc. should never be intercepted. The values reported if they were at best + // duplicate the values received from our bytecode instrumentation and at + // worst pollute the table of recent compares with string internal to the JDK. + if (!is_using_native_libraries) return false; + // If the fuzz target is using native libraries, intercept calls only if they + // don't originate from those address ranges that are known to belong to the + // JDK. + return std::none_of( + ignore_for_interception_ranges.cbegin(), + ignore_for_interception_ranges.cend(), + [caller_pc](const std::pair<uintptr_t, uintptr_t> &range) { + uintptr_t start; + uintptr_t end; + std::tie(start, end) = range; + auto address = reinterpret_cast<uintptr_t>(caller_pc); + return start <= address && address <= end; + }); +} + +[[maybe_unused]] void +Java_com_code_1intelligence_jazzer_runtime_TraceDataFlowNativeCallbacks_handleLibraryLoad( + JNIEnv *, jclass) { + std::call_once(ignore_list_flag, [] { + std::cout << "INFO: detected a native library load, enabling interception " + "for libc functions" + << std::endl; + for (const auto &lib_name : kLibrariesToIgnoreForInterception) + ignoreLibraryForInterception(lib_name); + // Enable the ignore list after it has been populated since vector is not + // thread-safe with respect to concurrent writes and reads. + is_using_native_libraries = true; + }); +} diff --git a/driver/sanitizer_hooks_with_pc.h b/driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_hooks_with_pc.h index d9861315..be655adb 100644 --- a/driver/sanitizer_hooks_with_pc.h +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_hooks_with_pc.h @@ -27,7 +27,9 @@ // associates it with particular coverage locations. // // Note: Only the lower 9 bits of the caller_pc argument are used by libFuzzer. +#ifdef __cplusplus extern "C" { +#endif void __sanitizer_cov_trace_cmp4_with_pc(void *caller_pc, uint32_t arg1, uint32_t arg2); void __sanitizer_cov_trace_cmp8_with_pc(void *caller_pc, uint64_t arg1, @@ -42,6 +44,6 @@ void __sanitizer_cov_trace_div8_with_pc(void *caller_pc, uint64_t val); void __sanitizer_cov_trace_gep_with_pc(void *caller_pc, uintptr_t idx); void __sanitizer_cov_trace_pc_indir_with_pc(void *caller_pc, uintptr_t callee); +#ifdef __cplusplus } - -void CalibrateTrampoline(); +#endif diff --git a/driver/sanitizer_symbols.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_symbols.cpp index 10255ef1..abc5f04e 100644 --- a/driver/sanitizer_symbols.cpp +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_symbols.cpp @@ -12,18 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Called in libfuzzer_driver.cpp. -extern "C" void __sanitizer_set_death_callback(void (*)()) {} - // Suppress libFuzzer warnings about missing sanitizer methods in non-sanitizer // builds. -extern "C" int __sanitizer_acquire_crash_state() { return 1; } +extern "C" [[maybe_unused]] int __sanitizer_acquire_crash_state() { return 1; } namespace jazzer { void DumpJvmStackTraces(); } // Dump a JVM stack trace on timeouts. -extern "C" void __sanitizer_print_stack_trace() { +extern "C" [[maybe_unused]] void __sanitizer_print_stack_trace() { jazzer::DumpJvmStackTraces(); } diff --git a/driver/src/main/native/com/code_intelligence/jazzer/driver/trigger_driver_hooks_load.cpp b/driver/src/main/native/com/code_intelligence/jazzer/driver/trigger_driver_hooks_load.cpp new file mode 100644 index 00000000..8e6d19ab --- /dev/null +++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/trigger_driver_hooks_load.cpp @@ -0,0 +1,50 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <dlfcn.h> +#include <jni.h> + +#include <cstdlib> + +// The native driver binary, if used, forwards all calls to native libFuzzer +// hooks such as __sanitizer_cov_trace_cmp8 to the Jazzer JNI library. In order +// to load the hook symbols when the library is ready, it needs to be passed a +// handle - the JVM loads libraries with RTLD_LOCAL and thus their symbols +// wouldn't be found as part of the global lookup procedure. +jint JNI_OnLoad(JavaVM *, void *) { + Dl_info info; + + if (!dladdr(reinterpret_cast<const void *>(&JNI_OnLoad), &info) || + !info.dli_fname) { + fprintf(stderr, "Failed to determine our dli_fname\n"); + abort(); + } + + void *handle = dlopen(info.dli_fname, RTLD_NOLOAD | RTLD_LAZY); + if (handle == nullptr) { + fprintf(stderr, "Failed to dlopen self: %s\n", dlerror()); + abort(); + } + + void *register_hooks = dlsym(RTLD_DEFAULT, "jazzer_initialize_native_hooks"); + // We may be running without the native driver, so not finding this method is + // an expected error. + if (register_hooks) { + reinterpret_cast<void (*)(void *)>(register_hooks)(handle); + } + + dlclose(handle); + + return JNI_VERSION_1_8; +} diff --git a/driver/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/driver/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel new file mode 100644 index 00000000..04119700 --- /dev/null +++ b/driver/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -0,0 +1,21 @@ +java_test( + name = "FuzzTargetRunnerTest", + srcs = ["FuzzTargetRunnerTest.java"], + jvm_flags = ["-ea"], + use_testrunner = False, + deps = [ + "//agent/src/main/java/com/code_intelligence/jazzer/api", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:coverage_map", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider", + "//driver/src/main/java/com/code_intelligence/jazzer/driver:fuzz_target_runner", + ], +) + +java_test( + name = "OptTest", + srcs = ["OptTest.java"], + deps = [ + "//driver/src/main/java/com/code_intelligence/jazzer/driver:opt", + "@maven//:junit_junit", + ], +) diff --git a/driver/src/test/java/com/code_intelligence/jazzer/driver/FuzzTargetRunnerTest.java b/driver/src/test/java/com/code_intelligence/jazzer/driver/FuzzTargetRunnerTest.java new file mode 100644 index 00000000..d8f048e5 --- /dev/null +++ b/driver/src/test/java/com/code_intelligence/jazzer/driver/FuzzTargetRunnerTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.runtime.CoverageMap; +import com.code_intelligence.jazzer.runtime.UnsafeProvider; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import sun.misc.Unsafe; + +public class FuzzTargetRunnerTest { + private static final Pattern DEDUP_TOKEN_PATTERN = + Pattern.compile("(?m)^DEDUP_TOKEN: ([0-9a-f]{16})$"); + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final ByteArrayOutputStream recordedErr = new ByteArrayOutputStream(); + private static final ByteArrayOutputStream recordedOut = new ByteArrayOutputStream(); + private static boolean fuzzerInitializeRan = false; + private static boolean finishedAllNonCrashingRuns = false; + + public static void fuzzerInitialize() { + fuzzerInitializeRan = true; + } + + public static void fuzzerTestOneInput(byte[] data) { + switch (new String(data, StandardCharsets.UTF_8)) { + case "no crash": + CoverageMap.recordCoverage(0); + return; + case "first finding": + CoverageMap.recordCoverage(1); + throw new IllegalArgumentException("first finding"); + case "second finding": + CoverageMap.recordCoverage(2); + Jazzer.reportFindingFromHook(new StackOverflowError("second finding")); + throw new IllegalArgumentException("not reported"); + case "crash": + CoverageMap.recordCoverage(3); + throw new IllegalArgumentException("crash"); + } + } + + public static void fuzzerTearDown() { + String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8); + assert errOutput.contains("== Java Exception: java.lang.IllegalArgumentException: crash"); + String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8); + assert DEDUP_TOKEN_PATTERN.matcher(outOutput).find(); + + assert finishedAllNonCrashingRuns : "Did not finish all expected runs before crashing"; + assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1, 2, 3).collect(Collectors.toSet())); + assert UNSAFE.getByte(CoverageMap.countersAddress) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 1; + // FuzzTargetRunner calls _Exit after this function, so the test would fail unless this line is + // executed. Use halt rather than exit to get around FuzzTargetRunner's shutdown hook calling + // fuzzerTearDown, which would otherwise result in a shutdown hook loop. + Runtime.getRuntime().halt(0); + } + + public static void main(String[] args) { + PrintStream recordingErr = new TeeOutputStream(new PrintStream(recordedErr, true), System.err); + System.setErr(recordingErr); + PrintStream recordingOut = new TeeOutputStream(new PrintStream(recordedOut, true), System.out); + System.setOut(recordingOut); + + System.setProperty("jazzer.target_class", FuzzTargetRunnerTest.class.getName()); + // Keep going past all "no crash", "first finding" and "second finding" runs, then crash. + System.setProperty("jazzer.keep_going", "3"); + + // Use a loop to simulate two findings with the same stack trace and thus verify that keep_going + // works as advertised. + for (int i = 1; i < 3; i++) { + int result = FuzzTargetRunner.runOne("no crash".getBytes(StandardCharsets.UTF_8)); + + assert result == 0; + assert !FuzzTargetRunner.useFuzzedDataProvider; + assert fuzzerInitializeRan; + assert CoverageMap.getCoveredIds().equals(Stream.of(0).collect(Collectors.toSet())); + assert UNSAFE.getByte(CoverageMap.countersAddress) == i; + assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 0; + assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 0; + assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0; + + String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8); + assert errOutput.isEmpty(); + String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8); + assert outOutput.isEmpty(); + } + + String firstDedupToken = null; + for (int i = 1; i < 3; i++) { + int result = FuzzTargetRunner.runOne("first finding".getBytes(StandardCharsets.UTF_8)); + + assert result == 0; + assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1).collect(Collectors.toSet())); + assert UNSAFE.getByte(CoverageMap.countersAddress) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == i; + assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 0; + assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0; + + String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8); + String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8); + if (i == 1) { + assert errOutput.contains( + "== Java Exception: java.lang.IllegalArgumentException: first finding"); + Matcher dedupTokenMatcher = DEDUP_TOKEN_PATTERN.matcher(outOutput); + assert dedupTokenMatcher.find(); + firstDedupToken = dedupTokenMatcher.group(); + recordedErr.reset(); + recordedOut.reset(); + } else { + assert errOutput.isEmpty(); + assert outOutput.isEmpty(); + } + } + + for (int i = 1; i < 3; i++) { + int result = FuzzTargetRunner.runOne("second finding".getBytes(StandardCharsets.UTF_8)); + + assert result == 0; + assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1, 2).collect(Collectors.toSet())); + assert UNSAFE.getByte(CoverageMap.countersAddress) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 2; + assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == i; + assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0; + + String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8); + String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8); + if (i == 1) { + // Verify that the StackOverflowError is wrapped in security issue and contains reproducer + // information. + assert errOutput.contains( + "== Java Exception: com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow: Stack overflow (use "); + assert !errOutput.contains("not reported"); + Matcher dedupTokenMatcher = DEDUP_TOKEN_PATTERN.matcher(outOutput); + assert dedupTokenMatcher.find(); + assert !firstDedupToken.equals(dedupTokenMatcher.group()); + recordedErr.reset(); + recordedOut.reset(); + } else { + assert errOutput.isEmpty(); + assert outOutput.isEmpty(); + } + } + + finishedAllNonCrashingRuns = true; + + FuzzTargetRunner.runOne("crash".getBytes(StandardCharsets.UTF_8)); + + throw new IllegalStateException("Expected FuzzTargetRunner to call fuzzerTearDown"); + } + + /** + * An OutputStream that prints to two OutputStreams simultaneously. + */ + private static class TeeOutputStream extends PrintStream { + private final PrintStream otherOut; + public TeeOutputStream(PrintStream out1, PrintStream out2) { + super(out1, true); + this.otherOut = out2; + } + + @Override + public void flush() { + super.flush(); + otherOut.flush(); + } + + @Override + public void close() { + super.close(); + otherOut.close(); + } + + @Override + public void write(int b) { + super.write(b); + otherOut.write(b); + } + + @Override + public void write(byte[] buf, int off, int len) { + super.write(buf, off, len); + otherOut.write(buf, off, len); + } + } +} diff --git a/driver/src/test/java/com/code_intelligence/jazzer/driver/OptTest.java b/driver/src/test/java/com/code_intelligence/jazzer/driver/OptTest.java new file mode 100644 index 00000000..87cda2b1 --- /dev/null +++ b/driver/src/test/java/com/code_intelligence/jazzer/driver/OptTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.driver; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.stream.Collectors; +import org.junit.Test; + +public class OptTest { + @Test + public void splitString() { + assertStringSplit("", ','); + assertStringSplit(",,,,,", ','); + assertStringSplit("fir\\\\st se\\ cond third", ' ', "fir\\st", "se cond", "third"); + assertStringSplit("first ", ' ', "first"); + assertStringSplit("first\\", ' ', "first"); + } + + @Test(expected = IllegalArgumentException.class) + public void splitString_noBackslashAsSeparator() { + assertStringSplit("foo", '\\'); + } + + public void assertStringSplit(String str, char sep, String... tokens) { + assertEquals(Arrays.stream(tokens).collect(Collectors.toList()), + Opt.splitOnUnescapedSeparator(str, sep)); + } +} diff --git a/driver/test_main.cpp b/driver/test_main.cpp index bf33517f..14340b87 100644 --- a/driver/test_main.cpp +++ b/driver/test_main.cpp @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include <rules_jni.h> + #include "gflags/gflags.h" #include "gtest/gtest.h" int main(int argc, char **argv) { + rules_jni_init(argv[0]); ::testing::InitGoogleTest(&argc, argv); gflags::ParseCommandLineFlags(&argc, &argv, true); return RUN_ALL_TESTS(); diff --git a/driver/testdata/BUILD.bazel b/driver/testdata/BUILD.bazel index 8dd67e12..c3c24431 100644 --- a/driver/testdata/BUILD.bazel +++ b/driver/testdata/BUILD.bazel @@ -3,8 +3,4 @@ java_binary( srcs = glob(["test/*.java"]), create_executable = False, visibility = ["//visibility:public"], - deps = [ - "//agent/src/main/java/com/code_intelligence/jazzer/api", - "//agent/src/main/java/com/code_intelligence/jazzer/runtime", - ], ) diff --git a/driver/testdata/test/FuzzTargetWithDataProvider.java b/driver/testdata/test/FuzzTargetWithDataProvider.java deleted file mode 100644 index fc5bc1b0..00000000 --- a/driver/testdata/test/FuzzTargetWithDataProvider.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package test; - -import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -class FuzzTargetWithDataProvider { - public static <T extends Comparable<T>> void assertEqual(T a, T b) { - if (a.compareTo(b) != 0) { - throw new IllegalArgumentException("Expected: " + a + ", got: " + b); - } - } - - public strictfp static void fuzzerTestOneInput(FuzzedDataProvider data) { - assertEqual(true, data.consumeBoolean()); - - assertEqual((byte) 0x7F, data.consumeByte()); - assertEqual((byte) 0x14, data.consumeByte((byte) 0x12, (byte) 0x22)); - - assertEqual(0x12345678, data.consumeInt()); - assertEqual(-0x12345600, data.consumeInt(-0x12345678, -0x12345600)); - assertEqual(0x12345679, data.consumeInt(0x12345678, 0x12345679)); - - assertEqual(true, Arrays.equals(new byte[] {0x01, 0x02}, data.consumeBytes(2))); - - assertEqual("jazzer", data.consumeString(6)); - assertEqual("ja\u0000zer", data.consumeString(6)); - assertEqual("€ß", data.consumeString(2)); - - assertEqual("jazzer", data.consumeAsciiString(6)); - assertEqual("ja\u0000zer", data.consumeAsciiString(6)); - assertEqual("\u0062\u0002\u002C\u0043\u001F", data.consumeAsciiString(5)); - - assertEqual(true, - Arrays.equals(new boolean[] {false, false, true, false, true}, data.consumeBooleans(5))); - assertEqual(true, - Arrays.equals(new long[] {0x0123456789abdcefL, 0xfedcba9876543210L}, data.consumeLongs(2))); - - assertEqual((float) 0.28969181, data.consumeProbabilityFloat()); - assertEqual(0.086814121166605432, data.consumeProbabilityDouble()); - assertEqual((float) 0.30104411, data.consumeProbabilityFloat()); - assertEqual(0.96218831486039413, data.consumeProbabilityDouble()); - - assertEqual((float) -2.8546307e+38, data.consumeRegularFloat()); - assertEqual(8.0940194040236032e+307, data.consumeRegularDouble()); - assertEqual((float) 271.49084, data.consumeRegularFloat((float) 123.0, (float) 777.0)); - assertEqual(30.859126145478349, data.consumeRegularDouble(13.37, 31.337)); - - assertEqual((float) 0.0, data.consumeFloat()); - assertEqual((float) -0.0, data.consumeFloat()); - assertEqual(Float.POSITIVE_INFINITY, data.consumeFloat()); - assertEqual(Float.NEGATIVE_INFINITY, data.consumeFloat()); - assertEqual(true, Float.isNaN(data.consumeFloat())); - assertEqual(Float.MIN_VALUE, data.consumeFloat()); - assertEqual(-Float.MIN_VALUE, data.consumeFloat()); - assertEqual(Float.MIN_NORMAL, data.consumeFloat()); - assertEqual(-Float.MIN_NORMAL, data.consumeFloat()); - assertEqual(Float.MAX_VALUE, data.consumeFloat()); - assertEqual(-Float.MAX_VALUE, data.consumeFloat()); - - assertEqual(0.0, data.consumeDouble()); - assertEqual(-0.0, data.consumeDouble()); - assertEqual(Double.POSITIVE_INFINITY, data.consumeDouble()); - assertEqual(Double.NEGATIVE_INFINITY, data.consumeDouble()); - assertEqual(true, Double.isNaN(data.consumeDouble())); - assertEqual(Double.MIN_VALUE, data.consumeDouble()); - assertEqual(-Double.MIN_VALUE, data.consumeDouble()); - assertEqual(Double.MIN_NORMAL, data.consumeDouble()); - assertEqual(-Double.MIN_NORMAL, data.consumeDouble()); - assertEqual(Double.MAX_VALUE, data.consumeDouble()); - assertEqual(-Double.MAX_VALUE, data.consumeDouble()); - - int[] array = {0, 1, 2, 3, 4}; - assertEqual(4, data.pickValue(array)); - assertEqual(2, (int) data.pickValue(Arrays.stream(array).boxed().toArray())); - assertEqual(3, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toList()))); - assertEqual(2, data.pickValue(Arrays.stream(array).boxed().collect(Collectors.toSet()))); - - // Buffer is almost depleted at this point. - assertEqual(7, data.remainingBytes()); - assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(3))); - assertEqual(7, data.remainingBytes()); - assertEqual(true, Arrays.equals(new int[] {0x12345678}, data.consumeInts(3))); - assertEqual(3, data.remainingBytes()); - assertEqual(0x123456L, data.consumeLong()); - - // Buffer has been fully consumed at this point - assertEqual(0, data.remainingBytes()); - assertEqual(0, data.consumeInt()); - assertEqual(0.0, data.consumeDouble()); - assertEqual(-13.37, data.consumeRegularDouble(-13.37, 31.337)); - assertEqual(true, Arrays.equals(new byte[0], data.consumeBytes(4))); - assertEqual(true, Arrays.equals(new long[0], data.consumeLongs(4))); - assertEqual("", data.consumeRemainingAsAsciiString()); - assertEqual("", data.consumeRemainingAsString()); - assertEqual("", data.consumeAsciiString(100)); - assertEqual("", data.consumeString(100)); - } -} diff --git a/driver/utils.cpp b/driver/utils.cpp deleted file mode 100644 index 4d8042e3..00000000 --- a/driver/utils.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "utils.h" - -#include <cstdint> -#include <cstring> -#include <iomanip> -#include <sstream> -#include <string> - -namespace { -// BEGIN: Obtained from https://github.com/x42/liboauth/blob/master/src/sha1.c: -/* This code is public-domain - it is based on libcrypt - * placed in the public domain by Wei Dai and other contributors. - */ - -#ifdef __BIG_ENDIAN__ -#define SHA_BIG_ENDIAN -#elif defined __LITTLE_ENDIAN__ -/* override */ -#elif defined __BYTE_ORDER -#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#define SHA_BIG_ENDIAN -#endif -#else // ! defined __LITTLE_ENDIAN__ -#include <endian.h> // machine/endian.h -#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#define SHA_BIG_ENDIAN -#endif -#endif - -/* header */ - -#define HASH_LENGTH 20 -#define BLOCK_LENGTH 64 - -typedef struct sha1nfo { - uint32_t buffer[BLOCK_LENGTH / 4]; - uint32_t state[HASH_LENGTH / 4]; - uint32_t byteCount; - uint8_t bufferOffset; - uint8_t keyBuffer[BLOCK_LENGTH]; - uint8_t innerHash[HASH_LENGTH]; -} sha1nfo; - -/* public API - prototypes - TODO: doxygen*/ - -/** - */ -void sha1_init(sha1nfo *s); -/** - */ -void sha1_writebyte(sha1nfo *s, uint8_t data); -/** - */ -void sha1_write(sha1nfo *s, const char *data, size_t len); -/** - */ -uint8_t *sha1_result(sha1nfo *s); - -/* code */ -#define SHA1_K0 0x5a827999 -#define SHA1_K20 0x6ed9eba1 -#define SHA1_K40 0x8f1bbcdc -#define SHA1_K60 0xca62c1d6 - -void sha1_init(sha1nfo *s) { - s->state[0] = 0x67452301; - s->state[1] = 0xefcdab89; - s->state[2] = 0x98badcfe; - s->state[3] = 0x10325476; - s->state[4] = 0xc3d2e1f0; - s->byteCount = 0; - s->bufferOffset = 0; -} - -uint32_t sha1_rol32(uint32_t number, uint8_t bits) { - return ((number << bits) | (number >> (32 - bits))); -} - -void sha1_hashBlock(sha1nfo *s) { - uint8_t i; - uint32_t a, b, c, d, e, t; - - a = s->state[0]; - b = s->state[1]; - c = s->state[2]; - d = s->state[3]; - e = s->state[4]; - for (i = 0; i < 80; i++) { - if (i >= 16) { - t = s->buffer[(i + 13) & 15] ^ s->buffer[(i + 8) & 15] ^ - s->buffer[(i + 2) & 15] ^ s->buffer[i & 15]; - s->buffer[i & 15] = sha1_rol32(t, 1); - } - if (i < 20) { - t = (d ^ (b & (c ^ d))) + SHA1_K0; - } else if (i < 40) { - t = (b ^ c ^ d) + SHA1_K20; - } else if (i < 60) { - t = ((b & c) | (d & (b | c))) + SHA1_K40; - } else { - t = (b ^ c ^ d) + SHA1_K60; - } - t += sha1_rol32(a, 5) + e + s->buffer[i & 15]; - e = d; - d = c; - c = sha1_rol32(b, 30); - b = a; - a = t; - } - s->state[0] += a; - s->state[1] += b; - s->state[2] += c; - s->state[3] += d; - s->state[4] += e; -} - -void sha1_addUncounted(sha1nfo *s, uint8_t data) { - uint8_t *const b = (uint8_t *)s->buffer; -#ifdef SHA_BIG_ENDIAN - b[s->bufferOffset] = data; -#else - b[s->bufferOffset ^ 3] = data; -#endif - s->bufferOffset++; - if (s->bufferOffset == BLOCK_LENGTH) { - sha1_hashBlock(s); - s->bufferOffset = 0; - } -} - -void sha1_writebyte(sha1nfo *s, uint8_t data) { - ++s->byteCount; - sha1_addUncounted(s, data); -} - -void sha1_write(sha1nfo *s, const char *data, size_t len) { - for (; len--;) sha1_writebyte(s, (uint8_t)*data++); -} - -void sha1_pad(sha1nfo *s) { - // Implement SHA-1 padding (fips180-2 §5.1.1) - - // Pad with 0x80 followed by 0x00 until the end of the block - sha1_addUncounted(s, 0x80); - while (s->bufferOffset != 56) sha1_addUncounted(s, 0x00); - - // Append length in the last 8 bytes - sha1_addUncounted(s, 0); // We're only using 32 bit lengths - sha1_addUncounted(s, 0); // But SHA-1 supports 64 bit lengths - sha1_addUncounted(s, 0); // So zero pad the top bits - sha1_addUncounted(s, s->byteCount >> 29); // Shifting to multiply by 8 - sha1_addUncounted( - s, s->byteCount >> 21); // as SHA-1 supports bitstreams as well as - sha1_addUncounted(s, s->byteCount >> 13); // byte. - sha1_addUncounted(s, s->byteCount >> 5); - sha1_addUncounted(s, s->byteCount << 3); -} - -uint8_t *sha1_result(sha1nfo *s) { - // Pad to complete the last block - sha1_pad(s); - -#ifndef SHA_BIG_ENDIAN - // Swap byte order back - int i; - for (i = 0; i < 5; i++) { - s->state[i] = (((s->state[i]) << 24) & 0xff000000) | - (((s->state[i]) << 8) & 0x00ff0000) | - (((s->state[i]) >> 8) & 0x0000ff00) | - (((s->state[i]) >> 24) & 0x000000ff); - } -#endif - - // Return pointer to hash (20 characters) - return (uint8_t *)s->state; -} -// END: Obtained from https://github.com/x42/liboauth/blob/master/src/sha1.c: -} // namespace - -namespace jazzer { -std::string Sha1Hash(const uint8_t *data, size_t size) { - sha1nfo hasher; - sha1_init(&hasher); - sha1_write(&hasher, reinterpret_cast<const char *>(data), size); - const uint8_t *hash = sha1_result(&hasher); - std::ostringstream out; - for (size_t i = 0; i < HASH_LENGTH; ++i) { - // Cast required because uint8_t would print as a char. - out << std::hex << std::setfill('0') << std::setw(2) - << static_cast<uint32_t>(hash[i]); - } - return out.str(); -} -} // namespace jazzer diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index dde8aaeb..599b8261 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -5,6 +5,7 @@ load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") java_fuzz_target_test( name = "Autofuzz", + expected_findings = ["java.lang.ArrayIndexOutOfBoundsException"], fuzzer_args = [ "--autofuzz=com.google.json.JsonSanitizer::sanitize", # Exit after the first finding for testing purposes. @@ -46,6 +47,8 @@ java_fuzz_target_test( fuzzer_args = ["--jvm_args=-Djazzer.native_lib=native_asan"], sanitizer = "address", target_class = "com.example.ExampleFuzzerWithNative", + target_compatible_with = SKIP_ON_WINDOWS, + verify_crash_reproducer = False, runtime_deps = [ ":example_fuzzer_with_native_lib", ], @@ -58,6 +61,7 @@ java_fuzz_target_test( target_class = "com.example.ExampleFuzzerWithNative", # Crashes at runtime without an error message. target_compatible_with = SKIP_ON_WINDOWS, + verify_crash_reproducer = False, runtime_deps = [ ":example_fuzzer_with_native_lib", ], @@ -78,16 +82,28 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/ExampleValueProfileFuzzer.java", ], + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], # Comment out the next line to keep the fuzzer running indefinitely. fuzzer_args = ["-use_value_profile=1"], target_class = "com.example.ExampleValueProfileFuzzer", ) java_fuzz_target_test( + name = "MazeFuzzer", + srcs = [ + "src/main/java/com/example/MazeFuzzer.java", + ], + expected_findings = ["com.example.MazeFuzzer$$TreasureFoundException"], + fuzzer_args = ["-use_value_profile=1"], + target_class = "com.example.MazeFuzzer", +) + +java_fuzz_target_test( name = "ExampleOutOfMemoryFuzzer", srcs = [ "src/main/java/com/example/ExampleOutOfMemoryFuzzer.java", ], + expected_findings = ["java.lang.OutOfMemoryError"], fuzzer_args = ["--jvm_args=-Xmx512m"], target_class = "com.example.ExampleOutOfMemoryFuzzer", ) @@ -97,6 +113,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/ExampleStackOverflowFuzzer.java", ], + expected_findings = ["java.lang.StackOverflowError"], target_class = "com.example.ExampleStackOverflowFuzzer", # Crashes with a segfault before any stack trace printing is reached. target_compatible_with = SKIP_ON_MACOS, @@ -126,18 +143,14 @@ java_fuzz_target_test( java_fuzz_target_test( name = "JpegImageParserFuzzer", + size = "enormous", srcs = [ "src/main/java/com/example/JpegImageParserFuzzer.java", ], + expected_findings = ["java.lang.NegativeArraySizeException"], fuzzer_args = [ - "-fork=5", - "--additional_jvm_args=-Dbaz=baz", - ] + select({ - # \\\\ becomes \\ when evaluated as a Starlark string literal, then \ in - # java_fuzz_target_test. - "@platforms//os:windows": ["--jvm_args=-Dfoo=foo;-Dbar=b\\\\;ar"], - "//conditions:default": ["--jvm_args=-Dfoo=foo:-Dbar=b\\\\:ar"], - }), + "-fork=2", + ], target_class = "com.example.JpegImageParserFuzzer", # The exit codes of the forked libFuzzer processes are not picked up correctly. target_compatible_with = SKIP_ON_MACOS, @@ -151,6 +164,11 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/GifImageParserFuzzer.java", ], + expected_findings = [ + "java.lang.ArrayIndexOutOfBoundsException", + "java.lang.IllegalArgumentException", + "java.lang.OutOfMemoryError", + ], target_class = "com.example.GifImageParserFuzzer", deps = [ "@maven//:org_apache_commons_commons_imaging", @@ -174,6 +192,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/JsonSanitizerCrashFuzzer.java", ], + expected_findings = ["java.lang.IndexOutOfBoundsException"], target_class = "com.example.JsonSanitizerCrashFuzzer", deps = [ "@maven//:com_mikesamuel_json_sanitizer", @@ -185,6 +204,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/JsonSanitizerDenylistFuzzer.java", ], + expected_findings = ["java.lang.AssertionError"], target_class = "com.example.JsonSanitizerDenylistFuzzer", deps = [ "@maven//:com_mikesamuel_json_sanitizer", @@ -225,6 +245,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/JsonSanitizerIdempotenceFuzzer.java", ], + expected_findings = ["java.lang.AssertionError"], target_class = "com.example.JsonSanitizerIdempotenceFuzzer", deps = [ "@maven//:com_mikesamuel_json_sanitizer", @@ -236,6 +257,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/JsonSanitizerValidJsonFuzzer.java", ], + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], target_class = "com.example.JsonSanitizerValidJsonFuzzer", deps = [ "@maven//:com_google_code_gson_gson", @@ -248,6 +270,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/JacksonCborFuzzer.java", ], + expected_findings = ["java.lang.NullPointerException"], target_class = "com.example.JacksonCborFuzzer", deps = [ "@maven//:com_fasterxml_jackson_core_jackson_core", @@ -261,6 +284,7 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/FastJsonFuzzer.java", ], + expected_findings = ["java.lang.NumberFormatException"], target_class = "com.example.FastJsonFuzzer", deps = [ "@maven//:com_alibaba_fastjson", @@ -280,6 +304,11 @@ kt_jvm_library( java_fuzz_target_test( name = "KlaxonFuzzer", + expected_findings = [ + "java.lang.ClassCastException", + "java.lang.IllegalStateException", + "java.lang.NumberFormatException", + ], fuzzer_args = [ "--keep_going=7", ], @@ -292,11 +321,12 @@ java_fuzz_target_test( srcs = [ "src/main/java/com/example/TurboJpegFuzzer.java", ], + data = [ + "@libjpeg_turbo//:turbojpeg_native", + ], fuzzer_args = [ "-rss_limit_mb=8196", - ], - native_libs = [ - "@libjpeg_turbo//:turbojpeg_native", + "--jvm_args=-Djava.library.path=../libjpeg_turbo", ], sanitizer = "address", tags = ["manual"], diff --git a/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java b/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java index acc023a2..b68ef6f7 100644 --- a/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java +++ b/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java @@ -32,14 +32,14 @@ public class ExampleValueProfileFuzzer { // Without -use_value_profile=1, the fuzzer gets stuck here as there is no direct correspondence // between the input bytes and the compared string. With value profile, the fuzzer can guess the // expected input byte by byte, which takes linear rather than exponential time. - if (base64(data.consumeBytes(6)).equals("SmF6emVy")) { + if (((Object) base64(data.consumeBytes(6))).equals("SmF6emVy")) { long[] plaintextBlocks = data.consumeLongs(2); if (plaintextBlocks.length != 2) return; if (insecureEncrypt(plaintextBlocks[0]) == 0x9fc48ee64d3dc090L) { - // Without --fake_pcs (enabled by default with -use_value_profile=1), the fuzzer would get - // stuck here as the value profile information for long comparisons would not be able to - // distinguish between this comparison and the one above. + // Without variants of the fuzzer hooks for compares that also take in fake PCs, the fuzzer + // would get stuck here as the value profile information for long comparisons would not be + // able to distinguish between this comparison and the one above. if (insecureEncrypt(plaintextBlocks[1]) == 0x888a82ff483ad9c2L) { mustNeverBeCalled(); } diff --git a/examples/src/main/java/com/example/JpegImageParserFuzzer.java b/examples/src/main/java/com/example/JpegImageParserFuzzer.java index a6898bf0..ba3e7c81 100644 --- a/examples/src/main/java/com/example/JpegImageParserFuzzer.java +++ b/examples/src/main/java/com/example/JpegImageParserFuzzer.java @@ -22,20 +22,6 @@ import org.apache.commons.imaging.formats.jpeg.JpegImageParser; // Found https://issues.apache.org/jira/browse/IMAGING-275. public class JpegImageParserFuzzer { - public static void fuzzerInitialize() { - String foo = System.getProperty("foo"); - String bar = System.getProperty("bar"); - String baz = System.getProperty("baz"); - // Only used to verify that arguments are correctly passed down to child processes. - if (foo == null || bar == null || baz == null || !foo.equals("foo") - || !(bar.equals("b;ar") || bar.equals("b:ar")) || !baz.equals("baz")) { - // Exit the process with an exit code different from that for a finding. - System.err.println("ERROR: Did not correctly pass all jvm_args to child process."); - System.err.printf("foo: %s%nbar: %s%nbaz: %s%n", foo, bar, baz); - System.exit(3); - } - } - public static void fuzzerTestOneInput(byte[] input) { try { new JpegImageParser().getBufferedImage(new ByteSourceArray(input), new HashMap<>()); diff --git a/examples/src/main/java/com/example/MazeFuzzer.java b/examples/src/main/java/com/example/MazeFuzzer.java new file mode 100644 index 00000000..9d3448c7 --- /dev/null +++ b/examples/src/main/java/com/example/MazeFuzzer.java @@ -0,0 +1,149 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.Consumer3; +import com.code_intelligence.jazzer.api.Jazzer; +import java.util.Arrays; +import java.util.stream.Collectors; + +// A fuzz target that shows how manually informing the fuzzer about important state can make a fuzz +// target much more effective. +// This is a Java version of the famous "maze game" discussed in +// "IJON: Exploring Deep State Spaces via Fuzzing", available at: +// https://wcventure.github.io/FuzzingPaper/Paper/SP20_IJON.pdf +public final class MazeFuzzer { + private static final String[] MAZE_STRING = new String[] { + " ███████████████████", + " █ █ █ █ █ █", + "█ █ █ █ ███ █ █ █ ███", + "█ █ █ █ █ █", + "█ █████ ███ ███ █ ███", + "█ █ █ █ █ █", + "█ ███ ███████ █ ███ █", + "█ █ █ █ █ █", + "███████ █ █ █████ ███", + "█ █ █ █ █", + "█ ███████ █ ███ ███ █", + "█ █ █ █ █ █ █", + "███ ███ █ ███ █ ███ █", + "█ █ █ █ █ █", + "█ ███████ █ █ █ █ █ █", + "█ █ █ █ █ █ █", + "█ █ █████████ ███ ███", + "█ █ █ █ █ █ █", + "█ █ █ ███ █████ ███ █", + "█ █ █ ", + "███████████████████ #", + }; + + private static final char[][] MAZE = parseMaze(); + private static final char[][] REACHED_FIELDS = parseMaze(); + + public static void fuzzerTestOneInput(byte[] commands) { + executeCommands(commands, (x, y, won) -> { + if (won) { + throw new TreasureFoundException(commands); + } + // This is the key line that makes this fuzz target work: It instructs the fuzzer to track + // every new combination of x and y as a new feature. Without it, the fuzzer would be + // completely lost in the maze as guessing an escaping path by chance is close to impossible. + Jazzer.exploreState(hash(x, y), 0); + if (REACHED_FIELDS[y][x] == ' ') { + // Fuzzer reached a new field in the maze, print its progress. + REACHED_FIELDS[y][x] = '.'; + System.out.println(renderMaze(REACHED_FIELDS)); + } + }); + } + + // Hash function with good mixing properties published by Thomas Mueller + // under the terms of CC BY-SA 4.0 at + // https://stackoverflow.com/a/12996028 + // https://creativecommons.org/licenses/by-sa/4.0/ + private static byte hash(byte x, byte y) { + int h = (x << 8) | y; + h = ((h >> 16) ^ h) * 0x45d9f3b; + h = ((h >> 16) ^ h) * 0x45d9f3b; + h = (h >> 16) ^ h; + return (byte) h; + } + + private static class TreasureFoundException extends RuntimeException { + TreasureFoundException(byte[] commands) { + super(renderPath(commands)); + } + } + + private static void executeCommands(byte[] commands, Consumer3<Byte, Byte, Boolean> callback) { + byte x = 0; + byte y = 0; + callback.accept(x, y, false); + + for (byte command : commands) { + byte nextX = x; + byte nextY = y; + switch (command) { + case 'L': + nextX--; + break; + case 'R': + nextX++; + break; + case 'U': + nextY--; + break; + case 'D': + nextY++; + break; + default: + return; + } + char nextFieldType; + try { + nextFieldType = MAZE[nextY][nextX]; + } catch (IndexOutOfBoundsException e) { + // Fuzzer tried to walk through the exterior walls of the maze. + continue; + } + if (nextFieldType != ' ' && nextFieldType != '#') { + // Fuzzer tried to walk through the interior walls of the maze. + continue; + } + // Fuzzer performed a valid move. + x = nextX; + y = nextY; + callback.accept(x, y, nextFieldType == '#'); + } + } + + private static char[][] parseMaze() { + return Arrays.stream(MazeFuzzer.MAZE_STRING).map(String::toCharArray).toArray(char[][] ::new); + } + + private static String renderMaze(char[][] maze) { + return Arrays.stream(maze).map(String::new).collect(Collectors.joining("\n", "\n", "\n")); + } + + private static String renderPath(byte[] commands) { + char[][] mutableMaze = parseMaze(); + executeCommands(commands, (x, y, won) -> { + if (!won) { + mutableMaze[y][x] = '.'; + } + }); + return renderMaze(mutableMaze); + } +} diff --git a/examples/src/main/native/com/example/BUILD.bazel b/examples/src/main/native/com/example/BUILD.bazel index 7f23f75e..4c44327a 100644 --- a/examples/src/main/native/com/example/BUILD.bazel +++ b/examples/src/main/native/com/example/BUILD.bazel @@ -9,6 +9,13 @@ cc_jni_library( "-fsanitize=fuzzer-no-link,address", "-fno-sanitize-blacklist", ], + defines = [ + # Workaround for Windows build failures with VS 2022: + # "lld-link: error: /INFERASANLIBS is not allowed in .drectve" + # https://github.com/llvm/llvm-project/issues/56300#issuecomment-1214313292 + "_DISABLE_STRING_ANNOTATION=1", + "_DISABLE_VECTOR_ANNOTATION=1", + ], linkopts = select({ "//:clang_on_linux": ["-fuse-ld=lld"], "@platforms//os:windows": [ @@ -1,9 +1,9 @@ # C++ & Java -find -name '*.cpp' -o -name '*.h' -o -name '*.java' | xargs clang-format-13 -i +find -name '*.cpp' -o -name '*.c' -o -name '*.h' -o -name '*.java' | xargs clang-format-13 -i # Kotlin # curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.42.1/ktlint && chmod a+x ktlint -ktlint -F "agent/**/*.kt" "driver/**/*.kt" "examples/**/*.kt" "sanitizers/**/*.kt" +ktlint -F "agent/**/*.kt" "driver/**/*.kt" "examples/**/*.kt" "sanitizers/**/*.kt" "tests/**/*.kt" # BUILD files # go get github.com/bazelbuild/buildtools/buildifier @@ -11,4 +11,4 @@ buildifier -r . # Licence headers # go get -u github.com/google/addlicense -addlicense -c "Code Intelligence GmbH" agent/ bazel/ deploy/ docker/ driver/ examples/ sanitizers/ *.bzl +addlicense -c "Code Intelligence GmbH" agent/ bazel/ deploy/ docker/ driver/ examples/ sanitizers/ tests/ *.bzl diff --git a/jazzer_setup.sh b/jazzer_setup.sh new file mode 100644 index 00000000..8fd2c9fe --- /dev/null +++ b/jazzer_setup.sh @@ -0,0 +1,6 @@ +#!/system/bin/sh +# Script to start "jazzer_setup" on the device +# +base=/system +export CLASSPATH=$base/framework/jazzer_setup.jar +exec app_process $base/bin com.jazzer.JazzerSetup "$@" @@ -14,25 +14,31 @@ load("@rules_jvm_external//:specs.bzl", "maven") -JAZZER_API_VERSION = "0.10.0" +JAZZER_API_VERSION = "0.11.0" JAZZER_API_COORDINATES = "com.code-intelligence:jazzer-api:%s" % JAZZER_API_VERSION # **WARNING**: These Maven dependencies have known vulnerabilities and are only used to test that # Jazzer finds these issues. DO NOT USE. MAVEN_ARTIFACTS = [ - "junit:junit:4.12", - "org.apache.commons:commons-imaging:1.0-alpha2", - "com.mikesamuel:json-sanitizer:1.2.1", - "com.google.code.gson:gson:2.8.6", + "com.alibaba:fastjson:1.2.75", + "com.beust:klaxon:5.5", "com.fasterxml.jackson.core:jackson-core:2.12.1", "com.fasterxml.jackson.core:jackson-databind:2.12.1", "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.1", - "com.alibaba:fastjson:1.2.75", - "com.beust:klaxon:5.5", + "com.github.jsqlparser:jsqlparser:4.4", # for SQL validation + "com.google.code.gson:gson:2.8.6", + "com.mikesamuel:json-sanitizer:1.2.1", + "com.unboundid:unboundid-ldapsdk:6.0.3", + "javax.el:javax.el-api:3.0.1-b06", "javax.validation:validation-api:2.0.1.Final", "javax.xml.bind:jaxb-api:2.3.1", - "javax.el:javax.el-api:3.0.1-b06", + "junit:junit:4.12", + "org.apache.commons:commons-imaging:1.0-alpha2", + "org.glassfish:javax.el:3.0.1-b06", "org.hibernate:hibernate-validator:5.2.4.Final", + "org.openjdk.jmh:jmh-core:1.34", + "org.openjdk.jmh:jmh-generator-annprocess:1.34", maven.artifact("org.apache.logging.log4j", "log4j-api", "2.14.1", testonly = True), maven.artifact("org.apache.logging.log4j", "log4j-core", "2.14.1", testonly = True), + maven.artifact("com.h2database", "h2", "2.1.212", testonly = True), ] diff --git a/maven_install.json b/maven_install.json index 16303c03..cda4c940 100644 --- a/maven_install.json +++ b/maven_install.json @@ -1,8 +1,8 @@ { "dependency_tree": { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": 993215468, - "__RESOLVED_ARTIFACTS_HASH": 40706841, + "__INPUT_ARTIFACTS_HASH": -921920920, + "__RESOLVED_ARTIFACTS_HASH": 43450148, "conflict_resolution": {}, "dependencies": [ { @@ -104,6 +104,17 @@ "url": "https://repo1.maven.org/maven2/com/fasterxml/classmate/1.1.0/classmate-1.1.0.jar" }, { + "coord": "com.github.jsqlparser:jsqlparser:4.4", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/com/github/jsqlparser/jsqlparser/4.4/jsqlparser-4.4.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/com/github/jsqlparser/jsqlparser/4.4/jsqlparser-4.4.jar" + ], + "sha256": "101e22917b22a339787fc85447ea057ea57b572e2a777a4628b6562354da117d", + "url": "https://repo1.maven.org/maven2/com/github/jsqlparser/jsqlparser/4.4/jsqlparser-4.4.jar" + }, + { "coord": "com.google.code.gson:gson:2.8.6", "dependencies": [], "directDependencies": [], @@ -115,6 +126,17 @@ "url": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.8.6/gson-2.8.6.jar" }, { + "coord": "com.h2database:h2:2.1.212", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/com/h2database/h2/2.1.212/h2-2.1.212.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/com/h2database/h2/2.1.212/h2-2.1.212.jar" + ], + "sha256": "db9284c6ff9bf3bc0087851edbd34563f1180df3ae87c67c5fe2203c0e67a536", + "url": "https://repo1.maven.org/maven2/com/h2database/h2/2.1.212/h2-2.1.212.jar" + }, + { "coord": "com.mikesamuel:json-sanitizer:1.2.1", "dependencies": [], "directDependencies": [], @@ -126,6 +148,17 @@ "url": "https://repo1.maven.org/maven2/com/mikesamuel/json-sanitizer/1.2.1/json-sanitizer-1.2.1.jar" }, { + "coord": "com.unboundid:unboundid-ldapsdk:6.0.3", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/6.0.3/unboundid-ldapsdk-6.0.3.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/6.0.3/unboundid-ldapsdk-6.0.3.jar" + ], + "sha256": "a635f130b482d8b02cc317632de762518d6bfedfecbd6972d1029124aaaf89d8", + "url": "https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/6.0.3/unboundid-ldapsdk-6.0.3.jar" + }, + { "coord": "javax.activation:javax.activation-api:1.2.0", "dependencies": [], "directDependencies": [], @@ -189,6 +222,17 @@ "url": "https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar" }, { + "coord": "net.sf.jopt-simple:jopt-simple:5.0.4", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar" + ], + "sha256": "df26cc58f235f477db07f753ba5a3ab243ebe5789d9f89ecf68dd62ea9a66c28", + "url": "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar" + }, + { "coord": "org.apache.commons:commons-imaging:1.0-alpha2", "dependencies": [], "directDependencies": [], @@ -200,6 +244,17 @@ "url": "https://repo1.maven.org/maven2/org/apache/commons/commons-imaging/1.0-alpha2/commons-imaging-1.0-alpha2.jar" }, { + "coord": "org.apache.commons:commons-math3:3.2", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/org/apache/commons/commons-math3/3.2/commons-math3-3.2.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.2/commons-math3-3.2.jar" + ], + "sha256": "6268a9a0ea3e769fc493a21446664c0ef668e48c93d126791f6f3f757978fee2", + "url": "https://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.2/commons-math3-3.2.jar" + }, + { "coord": "org.apache.logging.log4j:log4j-api:2.14.1", "dependencies": [], "directDependencies": [], @@ -226,6 +281,17 @@ "url": "https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar" }, { + "coord": "org.glassfish:javax.el:3.0.1-b06", + "dependencies": [], + "directDependencies": [], + "file": "v1/https/repo1.maven.org/maven2/org/glassfish/javax.el/3.0.1-b06/javax.el-3.0.1-b06.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/org/glassfish/javax.el/3.0.1-b06/javax.el-3.0.1-b06.jar" + ], + "sha256": "c255fe3ff4d7e491caf92c10c497f3c77d19acc4832d9bd2e80180d168fcedd2", + "url": "https://repo1.maven.org/maven2/org/glassfish/javax.el/3.0.1-b06/javax.el-3.0.1-b06.jar" + }, + { "coord": "org.hamcrest:hamcrest-core:1.3", "dependencies": [], "directDependencies": [], @@ -321,6 +387,40 @@ ], "sha256": "ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478", "url": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.jar" + }, + { + "coord": "org.openjdk.jmh:jmh-core:1.34", + "dependencies": [ + "net.sf.jopt-simple:jopt-simple:5.0.4", + "org.apache.commons:commons-math3:3.2" + ], + "directDependencies": [ + "net.sf.jopt-simple:jopt-simple:5.0.4", + "org.apache.commons:commons-math3:3.2" + ], + "file": "v1/https/repo1.maven.org/maven2/org/openjdk/jmh/jmh-core/1.34/jmh-core-1.34.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-core/1.34/jmh-core-1.34.jar" + ], + "sha256": "904384762d2ffeca8005aa9b432a7891a0e60c888bfd36f61dfcfa97c3a1d1b3", + "url": "https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-core/1.34/jmh-core-1.34.jar" + }, + { + "coord": "org.openjdk.jmh:jmh-generator-annprocess:1.34", + "dependencies": [ + "net.sf.jopt-simple:jopt-simple:5.0.4", + "org.openjdk.jmh:jmh-core:1.34", + "org.apache.commons:commons-math3:3.2" + ], + "directDependencies": [ + "org.openjdk.jmh:jmh-core:1.34" + ], + "file": "v1/https/repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-annprocess/1.34/jmh-generator-annprocess-1.34.jar", + "mirror_urls": [ + "https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-annprocess/1.34/jmh-generator-annprocess-1.34.jar" + ], + "sha256": "aa0feeefc0da59427b14c50139cba6deba211750e0033fdc39a5b3b8008b2900", + "url": "https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-annprocess/1.34/jmh-generator-annprocess-1.34.jar" } ], "version": "0.1.0" diff --git a/repositories.bzl b/repositories.bzl index 36b34443..7abffd87 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -14,69 +14,60 @@ """Contains the external dependencies required to build Jazzer (but not the examples).""" -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_jar") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") def jazzer_dependencies(): maybe( http_archive, name = "platforms", - sha256 = "079945598e4b6cc075846f7fd6a9d0857c33a7afc0de868c2ccb96405225135d", + sha256 = "379113459b0feaf6bfbb584a91874c065078aa673222846ac765f86661c27407", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.4/platforms-0.0.4.tar.gz", - "https://github.com/bazelbuild/platforms/releases/download/0.0.4/platforms-0.0.4.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz", ], ) maybe( http_archive, name = "bazel_skylib", - sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d", + sha256 = "f7be3474d42aae265405a592bb7da8e171919d74c16f082a5457840f06054728", urls = [ - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz", - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.2.1/bazel-skylib-1.2.1.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.2.1/bazel-skylib-1.2.1.tar.gz", ], ) maybe( http_archive, name = "io_bazel_rules_kotlin", - sha256 = "6cbd4e5768bdfae1598662e40272729ec9ece8b7bded8f0d2c81c8ff96dc139d", - url = "https://github.com/bazelbuild/rules_kotlin/releases/download/v1.5.0-beta-4/rules_kotlin_release.tgz", - ) - - maybe( - http_archive, - name = "com_google_glog", - repo_mapping = {"@com_github_gflags_gflags": "@jazzer_com_github_gflags_gflags"}, - sha256 = "5a39d51a6058348e6b683f5343a24d94e01c518c7a045101045e301a27efab13", - strip_prefix = "glog-a4a725d547a6c1329607db50af044c4fa329e07a", - url = "https://github.com/google/glog/archive/a4a725d547a6c1329607db50af044c4fa329e07a.tar.gz", + sha256 = "a57591404423a52bd6b18ebba7979e8cd2243534736c5c94d35c89718ea38f94", + url = "https://github.com/bazelbuild/rules_kotlin/releases/download/v1.6.0/rules_kotlin_release.tgz", ) maybe( http_archive, name = "com_google_absl", - sha256 = "5e1cbf25bf501f8e37866000a6052d02dbdd7b19a5b592251c59a4c9aa5c71ae", - strip_prefix = "abseil-cpp-f2dbd918d8d08529800eb72f23bd2829f92104a4", - url = "https://github.com/abseil/abseil-cpp/archive/f2dbd918d8d08529800eb72f23bd2829f92104a4.zip", + sha256 = "4208129b49006089ba1d6710845a45e31c59b0ab6bff9e5788a87f55c5abd602", + strip_prefix = "abseil-cpp-20220623.0", + url = "https://github.com/abseil/abseil-cpp/archive/refs/tags/20220623.0.tar.gz", ) maybe( http_archive, name = "com_github_johnynek_bazel_jar_jar", - sha256 = "97c5f862482a05f385bd8f9d28a9bbf684b0cf3fae93112ee96f3fb04d34b193", - strip_prefix = "bazel_jar_jar-171f268569384c57c19474b04aebe574d85fde0d", - url = "https://github.com/johnynek/bazel_jar_jar/archive/171f268569384c57c19474b04aebe574d85fde0d.tar.gz", + sha256 = "138a33a5c6ed9355e4411caa22f2fe45460b7e1e4468cbc29f7955367d7a001a", + strip_prefix = "bazel_jar_jar-commit-d97cfd22d47cba9a20708fa092f20348b72fb5ed", + url = "https://github.com/CodeIntelligenceTesting/bazel_jar_jar/archive/refs/tags/commit-d97cfd22d47cba9a20708fa092f20348b72fb5ed.tar.gz", ) maybe( http_archive, name = "com_github_jhalterman_typetools", build_file = Label("//third_party:typetools.BUILD"), - sha256 = "754f46de7d4c278cee2d4dba3c09ebe08fde03d0e67fc85d700611d9cdfb7868", - strip_prefix = "typetools-887153d2a9adf032fac9f145594d0a0248618d48", - url = "https://github.com/jhalterman/typetools/archive/887153d2a9adf032fac9f145594d0a0248618d48.tar.gz", + sha256 = "4e11a613aebb3c35deef58d5d942e44802da1a6c6ef7f127419261f00a0a082c", + strip_prefix = "typetools-commit-887153d2a9adf032fac9f145594d0a0248618d48", + url = "https://github.com/CodeIntelligenceTesting/typetools/archive/refs/tags/commit-887153d2a9adf032fac9f145594d0a0248618d48.tar.gz", ) maybe( @@ -91,17 +82,37 @@ def jazzer_dependencies(): maybe( http_archive, name = "fmeum_rules_jni", - sha256 = "8d685e381cb625e11fac330085de2ebc13ad497d30c4e9b09beb212f7c27e8e7", - url = "https://github.com/fmeum/rules_jni/releases/download/v0.3.0/rules_jni-v0.3.0.tar.gz", + sha256 = "47f0c566ef93fbca2fe94ae8b964d9bf2cb5b31be0efa66e9684b096e54042c1", + strip_prefix = "rules_jni-0.5.2", + url = "https://github.com/fmeum/rules_jni/archive/refs/tags/v0.5.2.tar.gz", ) maybe( - http_archive, - build_file = Label("//third_party:asm.BUILD"), - name = "jazzer_ow2_asm", - sha256 = "7b596cc584b241619911e99c5c96366fccd533b1a50b8720c151c2f74b5915e3", - strip_prefix = "asm-ASM_9_2", - url = "https://gitlab.ow2.org/asm/asm/-/archive/ASM_9_2/asm-ASM_9_2.tar.gz", + http_jar, + name = "net_bytebuddy_byte_buddy_agent", + sha256 = "25eed4301bbde3724a4bac0e7fe4a0b371c64b5fb40160b29480de3afd04efd5", + url = "https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy-agent/1.12.13/byte-buddy-agent-1.12.13.jar", + ) + + maybe( + http_jar, + name = "org_ow2_asm_asm", + sha256 = "1263369b59e29c943918de11d6d6152e2ec6085ce63e5710516f8c67d368e4bc", + url = "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.3/asm-9.3.jar", + ) + + maybe( + http_jar, + name = "org_ow2_asm_asm_commons", + sha256 = "a347c24732db2aead106b6e5996a015b06a3ef86e790a4f75b61761f0d2f7f39", + url = "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.3/asm-commons-9.3.jar", + ) + + maybe( + http_jar, + name = "org_ow2_asm_asm_tree", + sha256 = "ae629c2609f39681ef8d140a42a23800464a94f2d23e36d8f25cd10d5e4caff4", + url = "https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.3/asm-tree-9.3.jar", ) maybe( @@ -110,9 +121,9 @@ def jazzer_dependencies(): patches = [ Label("//third_party:gflags-use-double-dash-args.patch"), ], - sha256 = "ce2931dd537eaab7dab78b25bec6136a0756ca0b2acbdab9aec0266998c0d9a7", - strip_prefix = "gflags-827c769e5fc98e0f2a34c47cef953cc6328abced", - url = "https://github.com/gflags/gflags/archive/827c769e5fc98e0f2a34c47cef953cc6328abced.tar.gz", + sha256 = "34af2f15cf7367513b352bdcd2493ab14ce43692d2dcd9dfc499492966c64dcf", + strip_prefix = "gflags-2.2.2", + url = "https://github.com/gflags/gflags/archive/refs/tags/v2.2.2.tar.gz", ) maybe( @@ -123,15 +134,16 @@ def jazzer_dependencies(): Label("//third_party:jacoco-make-probe-adapter-subclassable.patch"), Label("//third_party:jacoco-make-probe-inserter-subclassable.patch"), ], - sha256 = "4a3c65b8a8ca58ffcec77288820f557ed93125e8a0b43dd7460b776c58bb8ed9", - strip_prefix = "jacoco-0.8.7-jazzer", - url = "https://github.com/CodeIntelligenceTesting/jacoco/archive/v0.8.7-jazzer.tar.gz", + sha256 = "c603cfcc5f3d95ecda46fb369dc54c82a453bb6b640a605c3970607d10896725", + strip_prefix = "jacoco-0.8.8", + url = "https://github.com/jacoco/jacoco/archive/refs/tags/v0.8.8.tar.gz", ) maybe( http_archive, name = "jazzer_libfuzzer", build_file = Label("//third_party:libFuzzer.BUILD"), - sha256 = "efde37ab5a9e4fff67f8cd43b701be5ea5ddb74a3bc10e4d8e91a614070145c3", - url = "https://github.com/CodeIntelligenceTesting/llvm-project-jazzer/releases/download/2021-11-30/jazzer-libfuzzer-2021-11-30.tar.gz", + sha256 = "3732ff706e5d049dbc76c2078d9e3ad265c6ccbe1b9ed749ae199df0f3118aac", + strip_prefix = "llvm-project-jazzer-2022-08-12/compiler-rt/lib/fuzzer", + url = "https://github.com/CodeIntelligenceTesting/llvm-project-jazzer/archive/refs/tags/2022-08-12.tar.gz", ) diff --git a/sanitizers/BUILD.bazel b/sanitizers/BUILD.bazel index fa84208e..fdc616a3 100644 --- a/sanitizers/BUILD.bazel +++ b/sanitizers/BUILD.bazel @@ -3,6 +3,5 @@ java_library( visibility = ["//visibility:public"], runtime_deps = [ "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers", - "//sanitizers/src/main/java/jaz", ], ) diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl index 8bdea7a9..cef4cf47 100644 --- a/sanitizers/sanitizers.bzl +++ b/sanitizers/sanitizers.bzl @@ -17,8 +17,13 @@ _sanitizer_package_prefix = "com.code_intelligence.jazzer.sanitizers." _sanitizer_class_names = [ "Deserialization", "ExpressionLanguageInjection", + "LdapInjection", "NamingContextLookup", + "OsCommandInjection", "ReflectiveCall", + "RegexInjection", + "RegexRoadblocks", + "SqlInjection", ] SANITIZER_CLASSES = [_sanitizer_package_prefix + class_name for class_name in _sanitizer_class_names] 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 65480653..1b156f9e 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 @@ -1,17 +1,34 @@ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +java_library( + name = "regex_roadblocks", + srcs = ["RegexRoadblocks.java"], + deps = [ + "//agent:jazzer_api_compile_only", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider", + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils:reflection_utils", + ], +) + kt_jvm_library( name = "sanitizers", srcs = [ "Deserialization.kt", "ExpressionLanguageInjection.kt", + "LdapInjection.kt", "NamingContextLookup.kt", + "OsCommandInjection.kt", "ReflectiveCall.kt", + "RegexInjection.kt", + "SqlInjection.kt", "Utils.kt", ], visibility = ["//sanitizers:__pkg__"], + runtime_deps = [ + ":regex_roadblocks", + ], deps = [ "//agent:jazzer_api_compile_only", - "//sanitizers/src/main/java/jaz", + "@maven//:com_github_jsqlparser_jsqlparser", ], ) 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 f6401dfd..55691c1a 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 @@ -29,7 +29,7 @@ import java.util.WeakHashMap /** * Detects unsafe deserialization that leads to attacker-controlled method calls, in particular to [Object.finalize]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object Deserialization { private val OBJECT_INPUT_STREAM_HEADER = 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 9b1e8ca6..1dc1d5f0 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 @@ -24,7 +24,7 @@ import java.lang.invoke.MethodHandle /** * Detects injectable inputs to an expression language interpreter which may lead to remote code execution. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ExpressionLanguageInjection { /** @@ -44,6 +44,16 @@ object ExpressionLanguageInjection { targetClassName = "javax.el.ExpressionFactory", targetMethod = "createMethodExpression", ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createValueExpression", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createMethodExpression", + ), ) @JvmStatic fun hookElExpressionFactory( @@ -52,10 +62,8 @@ object ExpressionLanguageInjection { arguments: Array<Any>, hookId: Int ) { - if (arguments[1] is String) { - val expression = arguments[1] as String - Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) - } + val expression = arguments[1] as? String ?: return + Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) } // With default configurations the argument to 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 new file mode 100644 index 00000000..1afd614e --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -0,0 +1,123 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.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 com.code_intelligence.jazzer.api.MethodHooks +import java.lang.Exception +import java.lang.invoke.MethodHandle +import javax.naming.NamingException +import javax.naming.directory.InvalidSearchFilterException + +/** + * Detects LDAP DN and search filter injections. + * + * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection + * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception + * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input + * was escaped correctly. + * + * Only the search methods are hooked, other methods are not used in injection attacks. Furthermore, + * only string parameters are checked, [javax.naming.Name] already validates inputs according to RFC2253. + * + * [javax.naming.directory.InitialDirContext] creates an initial context through the context factory + * stated in [javax.naming.Context.INITIAL_CONTEXT_FACTORY]. Other method calls are delegated to the + * initial context factory of type [javax.naming.directory.DirContext]. This is also the case for + * subclass [javax.naming.ldap.InitialLdapContext]. + */ +@Suppress("unused_parameter", "unused") +object LdapInjection { + + // Characters to escape in DNs + private const val NAME_CHARACTERS = "\\+<>,;\"=" + + // Characters to escape in search filter queries + private const val FILTER_CHARACTERS = "*()\\\u0000" + + @MethodHooks( + // Single object lookup, possible DN injection + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + 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, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljavax/naming/Name;Ljava/lang/String;[Ljava.lang.Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ) + ) + @JvmStatic + fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array<Any>, hookId: Int): Any? { + try { + return method.invokeWithArguments(thisObject, *args).also { + (args[0] as? String)?.let { name -> + Jazzer.guideTowardsEquality(name, NAME_CHARACTERS, hookId) + } + (args[1] as? String)?.let { filter -> + Jazzer.guideTowardsEquality(filter, FILTER_CHARACTERS, 31 * hookId) + } + } + } catch (e: Exception) { + when (e) { + is InvalidSearchFilterException -> + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """LDAP Injection +Search filters based on untrusted data must be escape as specified in RFC 4515.""" + ) + ) + is NamingException -> + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """LDAP Injection +Distinguished Names based on untrusted data must be escaped as specified in RFC 2253.""" + ) + ) + } + throw e + } + } +} 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 2d4fb9cf..56e12f03 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 @@ -22,6 +22,7 @@ import com.code_intelligence.jazzer.api.MethodHooks import java.lang.invoke.MethodHandle import javax.naming.CommunicationException +@Suppress("unused") object NamingContextLookup { // The particular URL g.co is used here since it is: @@ -31,6 +32,7 @@ object NamingContextLookup { private const val LDAP_MARKER = "ldap://g.co/" private const val RMI_MARKER = "rmi://g.co/" + @Suppress("UNUSED_PARAMETER") @MethodHooks( MethodHook( type = HookType.REPLACE, @@ -40,46 +42,10 @@ object NamingContextLookup { ), MethodHook( type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, targetClassName = "javax.naming.Context", targetMethod = "lookupLink", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), ) @JvmStatic fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any { 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 new file mode 100644 index 00000000..d3adc207 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt @@ -0,0 +1,61 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.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.lang.invoke.MethodHandle + +/** + * Detects unsafe execution of OS commands using [ProcessBuilder]. + * Executing OS commands based on attacker-controlled data could lead to arbitrary could execution. + * + * All public methods providing the command to execute end up in [java.lang.ProcessImpl.start], + * so calls to this method are hooked. + * Only the first entry of the given command array is analyzed. It states the executable and must + * not include attacker provided data. + */ +@Suppress("unused_parameter", "unused") +object OsCommandInjection { + + // Short and probably non-existing command name + private const val COMMAND = "jazze" + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ProcessImpl", + targetMethod = "start", + additionalClassesToHook = ["java.lang.ProcessBuilder"] + ) + @JvmStatic + fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + // Calling ProcessBuilder already checks if command array is empty + @Suppress("UNCHECKED_CAST") + (args[0] as? Array<String>)?.first().let { cmd -> + if (cmd == COMMAND) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """OS Command Injection +Executing OS commands with attacker-controlled data can lead to remote code execution.""" + ) + ) + } else { + Jazzer.guideTowardsEquality(cmd, COMMAND, hookId) + } + } + } +} 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 7842d879..0fcabe36 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 @@ -14,21 +14,59 @@ package com.code_intelligence.jazzer.sanitizers +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh import com.code_intelligence.jazzer.api.HookType import com.code_intelligence.jazzer.api.Jazzer import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks import java.lang.invoke.MethodHandle /** - * Detects unsafe reflective calls that lead to attacker-controlled method calls. + * Detects unsafe calls that lead to attacker-controlled class loading. + * + * Guide the fuzzer to load honeypot class via [Class.forName] or [ClassLoader.loadClass]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ReflectiveCall { - @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName") + @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;"), + ) @JvmStatic - fun classForNameHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + fun loadClassHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, 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;"), + ) + @JvmStatic + fun loadClassWithModuleHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val className = args[1] as? String ?: return + Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) + } + + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "mapLibraryName"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "findLibrary"), + ) + @JvmStatic + fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val libraryName = args[0] as? String ?: return + if (libraryName == HONEYPOT_LIBRARY_NAME) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueHigh("load arbitrary library") + ) + } + Jazzer.guideTowardsEquality(libraryName, HONEYPOT_LIBRARY_NAME, hookId) + } } 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 new file mode 100644 index 00000000..def5f6e3 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt @@ -0,0 +1,160 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.invoke.MethodHandle +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +@Suppress("unused_parameter", "unused") +object RegexInjection { + /** + * Part of an OOM "exploit" for [java.util.regex.Pattern.compile] with the + * [java.util.regex.Pattern.CANON_EQ] flag, formed by three consecutive combining marks, in this + * case grave accents: ◌̀. + * See [compileWithFlagsHook] for details. + */ + private const val CANON_EQ_ALMOST_EXPLOIT = "\u0300\u0300\u0300" + + /** + * When injected into a regex pattern, helps the fuzzer break out of quotes and character + * classes in order to cause a [PatternSyntaxException]. + */ + private const val FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN = "\\E]\\E]]]]]]" + + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;" + ) + @JvmStatic + fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, 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) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;" + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "matches", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z" + ), + ) + @JvmStatic + fun patternHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, *args) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "matches", + targetMethodDescriptor = "(Ljava/lang/String;)Z", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceAll", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceFirst", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/lang/String;", + ), + ) + @JvmStatic + fun stringHook(method: MethodHandle, thisObject: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, thisObject, *args) + } + + private fun hookInternal( + method: MethodHandle, + pattern: String?, + hasCanonEqFlag: Boolean, + hookId: Int, + vararg args: Any? + ): Any? { + if (hasCanonEqFlag && pattern != null) { + // With CANON_EQ enabled, Pattern.compile allocates an array with a size that is + // (super-)exponential in the number of consecutive Unicode combining marks. We use a mild case + // of this as a magic string based on which we trigger a finding. + // Note: The fuzzer might trigger an OutOfMemoryError or NegativeArraySizeException (if the size + // of the array overflows an int) by chance before it correctly emits this "exploit". In that + // case, we report the original exception instead. + if (pattern.contains(CANON_EQ_ALMOST_EXPLOIT)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueLow( + """Regular Expression Injection with CANON_EQ +When java.util.regex.Pattern.compile is used with the Pattern.CANON_EQ flag, +every injection into the regular expression pattern can cause arbitrarily large +memory allocations, even when wrapped with Pattern.quote(...).""" + ) + ) + } else { + Jazzer.guideTowardsContainment(pattern, CANON_EQ_ALMOST_EXPLOIT, hookId) + } + } + try { + return method.invokeWithArguments(*args).also { + // Only submit a fuzzer hint if no exception has been thrown. + if (!hasCanonEqFlag && pattern != null) { + Jazzer.guideTowardsContainment(pattern, FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN, hookId) + } + } + } catch (e: Exception) { + if (e is PatternSyntaxException) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueLow( + """Regular Expression Injection +Regular expression patterns that contain unescaped untrusted input can consume +arbitrary amounts of CPU time. To properly escape the input, wrap it with +Pattern.quote(...).""", + e + ) + ) + } + throw e + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java new file mode 100644 index 00000000..1043ac02 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java @@ -0,0 +1,322 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers; + +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.INVALID_OFFSET; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.field; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.nestedClass; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.offset; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import com.code_intelligence.jazzer.runtime.UnsafeProvider; +import java.lang.invoke.MethodHandle; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import sun.misc.Unsafe; + +/** + * The hooks in this class extend the reach of Jazzer's string compare instrumentation to literals + * (both strings and characters) that are part of regular expression patterns. + * <p> + * Internally, the Java standard library represents a compiled regular expression as a graph of + * instances of Pattern$Node instances, each of which represents a single unit of the full + * expression and provides a `match` function that takes a {@link Matcher}, a {@link CharSequence} + * to match against and an index into the sequence. With a hook on this method for every subclass of + * Pattern$Node, the contents of the node can be inspected and an appropriate string comparison + * between the relevant part of the input string and the literal string can be reported. + */ +public final class RegexRoadblocks { + // The number of characters preceding one that failed a character predicate to include in the + // reported string comparison. + private static final int CHARACTER_COMPARE_CONTEXT_LENGTH = 10; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final Class<?> SLICE_NODE = nestedClass(Pattern.class, "SliceNode"); + private static final long SLICE_NODE_BUFFER_OFFSET = + offset(UNSAFE, field(SLICE_NODE, "buffer", int[].class)); + private static final Class<?> CHAR_PREDICATE = nestedClass(Pattern.class, "CharPredicate"); + private static final Class<?> CHAR_PROPERTY = nestedClass(Pattern.class, "CharProperty"); + private static final long CHAR_PROPERTY_PREDICATE_OFFSET = offset( + UNSAFE, field(CHAR_PROPERTY, "predicate", nestedClass(Pattern.class, "CharPredicate"))); + private static final Class<?> BIT_CLASS = nestedClass(Pattern.class, "BitClass"); + private static final long BIT_CLASS_BITS_OFFSET = + offset(UNSAFE, field(BIT_CLASS, "bits", boolean[].class)); + + // Weakly map CharPredicate instances to characters that satisfy the predicate. Since + // CharPredicate instances are usually lambdas, we collect their solutions by hooking the + // functions constructing them rather than extracting the solutions via reflection. + // Note: Java 8 uses anonymous subclasses of CharProperty instead of lambdas implementing + // CharPredicate, hence CharProperty instances are used as keys instead in that case. + private static final ThreadLocal<WeakHashMap<Object, Character>> PREDICATE_SOLUTIONS = + ThreadLocal.withInitial(WeakHashMap::new); + + // Do not act on instrumented regexes used by Jazzer internally, e.g. by ClassGraph. + private static boolean HOOK_DISABLED = true; + + static { + Jazzer.onFuzzTargetReady(() -> HOOK_DISABLED = UNSAFE == null); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Node", + targetMethod = "match", + targetMethodDescriptor = "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z", + additionalClassesToHook = + { + "java.util.regex.Matcher", + "java.util.regex.Pattern$BackRef", + "java.util.regex.Pattern$Behind", + "java.util.regex.Pattern$BehindS", + "java.util.regex.Pattern$BmpCharProperty", + "java.util.regex.Pattern$BmpCharPropertyGreedy", + "java.util.regex.Pattern$BnM", + "java.util.regex.Pattern$BnMS", + "java.util.regex.Pattern$Bound", + "java.util.regex.Pattern$Branch", + "java.util.regex.Pattern$BranchConn", + "java.util.regex.Pattern$CharProperty", + "java.util.regex.Pattern$CharPropertyGreedy", + "java.util.regex.Pattern$CIBackRef", + "java.util.regex.Pattern$Caret", + "java.util.regex.Pattern$Curly", + "java.util.regex.Pattern$Conditional", + "java.util.regex.Pattern$First", + "java.util.regex.Pattern$GraphemeBound", + "java.util.regex.Pattern$GroupCurly", + "java.util.regex.Pattern$GroupHead", + "java.util.regex.Pattern$GroupRef", + "java.util.regex.Pattern$LastMatch", + "java.util.regex.Pattern$LazyLoop", + "java.util.regex.Pattern$LineEnding", + "java.util.regex.Pattern$Loop", + "java.util.regex.Pattern$Neg", + "java.util.regex.Pattern$NFCCharProperty", + "java.util.regex.Pattern$NotBehind", + "java.util.regex.Pattern$NotBehindS", + "java.util.regex.Pattern$Pos", + "java.util.regex.Pattern$Ques", + "java.util.regex.Pattern$Slice", + "java.util.regex.Pattern$SliceI", + "java.util.regex.Pattern$SliceIS", + "java.util.regex.Pattern$SliceS", + "java.util.regex.Pattern$SliceU", + "java.util.regex.Pattern$Start", + "java.util.regex.Pattern$StartS", + "java.util.regex.Pattern$UnixCaret", + "java.util.regex.Pattern$UnixDollar", + "java.util.regex.Pattern$XGrapheme", + }) + public static void + nodeMatchHook(MethodHandle method, Object node, Object[] args, int hookId, Boolean matched) { + if (HOOK_DISABLED || matched || node == null) + return; + Matcher matcher = (Matcher) args[0]; + if (matcher == null) + return; + int i = (int) args[1]; + CharSequence seq = (CharSequence) args[2]; + if (seq == null) + return; + + if (SLICE_NODE != null && SLICE_NODE.isInstance(node)) { + // The node encodes a match against a fixed string literal. Extract the literal and report a + // comparison between it and the subsequence of seq starting at i. + if (SLICE_NODE_BUFFER_OFFSET == INVALID_OFFSET) + return; + int currentLength = limitedLength(matcher.regionEnd() - i); + String current = seq.subSequence(i, i + currentLength).toString(); + + // All the subclasses of SliceNode store the literal in an int[], which we have to truncate to + // a char[]. + int[] buffer = (int[]) UNSAFE.getObject(node, SLICE_NODE_BUFFER_OFFSET); + char[] charBuffer = new char[limitedLength(buffer.length)]; + for (int j = 0; j < charBuffer.length; j++) { + charBuffer[j] = (char) buffer[j]; + } + String target = new String(charBuffer); + + Jazzer.guideTowardsEquality(current, target, perRegexId(hookId, matcher)); + } else if (CHAR_PROPERTY != null && CHAR_PROPERTY.isInstance(node)) { + // The node encodes a match against a class of characters, which may be hard to guess unicode + // characters. We rely on further hooks to track the relation between these nodes and + // characters satisfying their match function since the nodes themselves encode this + // information in lambdas, which are difficult to dissect via reflection. If we know a + // matching character, report a one-character (plus context) string comparison. + Object solutionKey; + if (CHAR_PROPERTY_PREDICATE_OFFSET == INVALID_OFFSET) { + if (CHAR_PREDICATE == null) { + // We are likely running against JDK 8, which directly construct subclasses of + // CharProperty rather than using lambdas implementing CharPredicate. + solutionKey = node; + } else { + return; + } + } else { + solutionKey = UNSAFE.getObject(node, CHAR_PROPERTY_PREDICATE_OFFSET); + } + if (solutionKey == null) + return; + Character solution = predicateSolution(solutionKey); + if (solution == null) + return; + // We report a string comparison rather than an integer comparison for two reasons: + // 1. If the characters are four byte codepoints, they will be coded on six bytes (a surrogate + // pair) in CESU-8, which is the encoding assumed for the fuzzer input, whereas ASCII + // characters will be coded on a single byte. By using the string compare hook, we do not + // have to worry about the encoding at this point. + // 2. The same character can appear multiple times in both the pattern and the matched string, + // which makes it harder for the fuzzer to determine the correct position to mutate the + // current character into the matching character. By providing a short section of the + // input string preceding the incorrect character, we increase the chance of a hit. + String context = + seq.subSequence(Math.max(0, i - CHARACTER_COMPARE_CONTEXT_LENGTH), i).toString(); + String current = seq.subSequence(i, Math.min(i + 1, matcher.regionEnd())).toString(); + String target = Character.toString(solution); + Jazzer.guideTowardsEquality(context + current, context + target, perRegexId(hookId, matcher)); + } + } + + // This and all following hooks track the relation between a CharPredicate or CharProperty + // instance and a character that matches it. We use an after hook on the factory methods so that + // we have access to the parameters and the created instance at the same time. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Single", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$BmpCharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleI", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleS", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleU", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + singleHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + // Java 8 uses classes extending CharProperty instead of lambdas implementing CharPredicate to + // match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Single", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleI", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleS", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleU", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + java8SingleHook( + MethodHandle method, Object property, Object[] args, int hookId, Object alwaysNull) { + if (HOOK_DISABLED || property == null) + return; + PREDICATE_SOLUTIONS.get().put(property, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Range", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRange", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRangeU", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "rangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "caseInsensitiveRangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + rangeHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$CharPredicate", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharPredicate;)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook union for those. Even though the classes + // of the parameters will be different, the actual implementation of the hook is the same in this + // case. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharProperty;Ljava/util/regex/Pattern$CharProperty;)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + unionHook( + MethodHandle method, Object thisObject, Object[] args, int hookId, Object unionPredicate) { + if (HOOK_DISABLED || unionPredicate == null) + return; + Character solution = predicateSolution(thisObject); + if (solution == null) + solution = predicateSolution(args[0]); + if (solution == null) + return; + PREDICATE_SOLUTIONS.get().put(unionPredicate, solution); + } + + private static Character predicateSolution(Object charPredicate) { + return PREDICATE_SOLUTIONS.get().computeIfAbsent(charPredicate, unused -> { + if (BIT_CLASS != null && BIT_CLASS.isInstance(charPredicate)) { + // BitClass instances have an empty bits array at construction time, so we scan their + // constants lazily when needed. + boolean[] bits = (boolean[]) UNSAFE.getObject(charPredicate, BIT_CLASS_BITS_OFFSET); + for (int i = 0; i < bits.length; i++) { + if (bits[i]) { + PREDICATE_SOLUTIONS.get().put(charPredicate, (char) i); + return (char) i; + } + } + } + return null; + }); + } + + // Limits a length to the maximum length libFuzzer will read up to in a callback. + private static int limitedLength(int length) { + return Math.min(length, 64); + } + + // hookId only takes one distinct value per Node subclass. In order to get different regex matches + // to be tracked similar to different instances of string compares, we mix in the hash of the + // underlying pattern. We expect patterns to be static almost always, so that this should not fill + // up the value profile map too quickly. + private static int perRegexId(int hookId, Matcher matcher) { + return hookId ^ matcher.pattern().toString().hashCode(); + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt new file mode 100644 index 00000000..f317bcc8 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt @@ -0,0 +1,113 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import net.sf.jsqlparser.JSQLParserException +import net.sf.jsqlparser.parser.CCJSqlParserUtil +import java.lang.invoke.MethodHandle + +/** + * Detects SQL injections. + * + * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection + * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception + * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input + * was escaped correctly. + * + * Two types of methods are hooked: + * 1. Methods that take an SQL query as the first argument (e.g. [java.sql.Statement.execute]). + * 2. Methods that don't take any arguments and execute an already prepared statement + * (e.g. [java.sql.PreparedStatement.execute]). + * For 1. we validate the syntax of the query using <a href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> + * and if both the syntax is invalid and the query execution throws an exception we report an SQL injection. + * Since we can't reliably validate SQL queries in arbitrary dialects this hook is expected to produce some + * amount of false positives. + * For 2. we can't validate the query syntax and therefore only rethrow any exceptions. + */ +@Suppress("unused_parameter", "unused") +object SqlInjection { + + // Characters that should be escaped in user input. + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + private const val CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_" + + private val SQL_SYNTAX_ERROR_EXCEPTIONS = listOf( + "java.sql.SQLException", + "java.sql.SQLNonTransientException", + "java.sql.SQLSyntaxErrorException", + "org.h2.jdbc.JdbcSQLSyntaxErrorException", + ) + + @MethodHooks( + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeBatch"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeBatch"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeUpdate"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeQuery"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeUpdate"), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.persistence.EntityManager", + targetMethod = "createNativeQuery" + ) + ) + @JvmStatic + fun checkSqlExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any { + var hasValidSqlQuery = false + + if (arguments.isNotEmpty() && arguments[0] is String) { + val query = arguments[0] as String + hasValidSqlQuery = isValidSql(query) + Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId) + } + return try { + method.invokeWithArguments(thisObject, *arguments) + } catch (throwable: Throwable) { + // If we already validated the query string and know it's correct, + // The exception is likely thrown by a non-existent table or something + // that we don't want to report. + if (!hasValidSqlQuery && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.javaClass.name)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueHigh( + """ + SQL Injection + Injected query: ${arguments[0]} + """.trimIndent(), + throwable + ) + ) + } + throw throwable + } + } + + private fun isValidSql(sql: String): Boolean = + try { + CCJSqlParserUtil.parseStatements(sql) + true + } catch (e: JSQLParserException) { + false + } catch (t: Throwable) { + // Catch any unexpected exceptions so that we don't disturb the + // instrumented application. + t.printStackTrace() + true + } +} 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 3166773b..219490d8 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 @@ -21,6 +21,7 @@ import java.io.InputStream * jaz.Zer is a honeypot class: All of its methods report a finding when called. */ const val HONEYPOT_CLASS_NAME = "jaz.Zer" +const val HONEYPOT_LIBRARY_NAME = "jazzer_honeypot" internal fun Short.toBytes(): ByteArray { return byteArrayOf( @@ -43,9 +44,20 @@ internal fun ByteArray.indexOf(needle: ByteArray): Int { } 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) { + val count = stream.read(current, n, size - n) + if (count < 0) break + n += count + } + return current + } + check(stream.markSupported()) stream.mark(target.size) - val current = stream.readNBytes(target.size) + val current = readBytes(stream, target.size) stream.reset() Jazzer.guideTowardsEquality(current, target, id) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel new file mode 100644 index 00000000..c7258447 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel @@ -0,0 +1,7 @@ +java_library( + name = "reflection_utils", + srcs = ["ReflectionUtils.java"], + visibility = [ + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", + ], +) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java new file mode 100644 index 00000000..fd6ac72f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java @@ -0,0 +1,62 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers.utils; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class ReflectionUtils { + public static final long INVALID_OFFSET = Long.MIN_VALUE; + + private static final boolean JAZZER_REFLECTION_DEBUG = + "1".equals(System.getenv("JAZZER_REFLECTION_DEBUG")); + + public static Class<?> clazz(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static Class<?> nestedClass(Class<?> parentClass, String nestedClassName) { + return clazz(parentClass.getName() + "$" + nestedClassName); + } + + public static Field field(Class<?> clazz, String name, Class<?> type) { + if (clazz == null) + return null; + try { + Field field = clazz.getDeclaredField(name); + if (!field.getType().equals(type)) { + throw new NoSuchFieldException( + "Expected " + name + " to be of type " + type + " (is: " + field.getType() + ")"); + } + return field; + } catch (NoSuchFieldException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static long offset(Unsafe unsafe, Field field) { + if (unsafe == null || field == null) + return INVALID_OFFSET; + return unsafe.objectFieldOffset(field); + } +} diff --git a/sanitizers/src/main/java/jaz/BUILD.bazel b/sanitizers/src/main/java/jaz/BUILD.bazel deleted file mode 100644 index 81275a31..00000000 --- a/sanitizers/src/main/java/jaz/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -java_library( - name = "jaz", - srcs = [ - "Ter.java", - "Zer.java", - ], - visibility = [ - "//sanitizers:__pkg__", - "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", - ], - deps = ["//agent:jazzer_api_compile_only"], -) diff --git a/sanitizers/src/main/java/jaz/Zer.java b/sanitizers/src/main/java/jaz/Zer.java deleted file mode 100644 index 0b27609c..00000000 --- a/sanitizers/src/main/java/jaz/Zer.java +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2021 Code Intelligence GmbH -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package jaz; - -import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; -import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; -import com.code_intelligence.jazzer.api.Jazzer; -import java.io.IOException; -import java.io.ObjectInputStream; - -/** - * A honeypot class that reports an appropriate finding on any interaction with one of its methods - * or initializers. - * - * Note: This class must not be referenced in any way by the rest of the code, not even statically. - * When referring to it, always use its hardcoded class name "jaz.Zer". - */ -@SuppressWarnings("unused") -public class Zer implements java.io.Serializable { - static final long serialVersionUID = 42L; - - private static final Throwable staticInitializerCause; - - static { - staticInitializerCause = new FuzzerSecurityIssueMedium("finalize call on arbitrary object"); - } - - public Zer() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("default constructor call on arbitrary object")); - } - - public Zer(String arg1) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("String constructor call on arbitrary object")); - } - - public Zer(String arg1, Throwable arg2) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("(String, Throwable) constructor call on arbitrary object")); - } - - private String jaz; - - public String getJaz() { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("getter call on arbitrary object")); - return jaz; - } - - public void setJaz(String jaz) { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("setter call on arbitrary object")); - this.jaz = jaz; - } - - @Override - public int hashCode() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("hashCode call on arbitrary object")); - return super.hashCode(); - } - - @Override - public boolean equals(Object obj) { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("equals call on arbitrary object")); - return super.equals(obj); - } - - @Override - protected Object clone() throws CloneNotSupportedException { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("clone call on arbitrary object")); - return super.clone(); - } - - @Override - public String toString() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("toString call on arbitrary object")); - return super.toString(); - } - - @Override - protected void finalize() throws Throwable { - // finalize is invoked automatically by the GC with an uninformative stack trace. We use the - // stack trace prerecorded in the static initializer. - Jazzer.reportFindingFromHook(staticInitializerCause); - super.finalize(); - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n" - + " Deserialization of arbitrary classes with custom readObject may allow remote\n" - + " code execution depending on the classpath.")); - in.defaultReadObject(); - } -} diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index d148545a..5d2e1ca5 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -1,10 +1,12 @@ load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") +load("//bazel:compat.bzl", "SKIP_ON_MACOS") java_fuzz_target_test( name = "ObjectInputStreamDeserialization", srcs = [ "ObjectInputStreamDeserialization.java", ], + expected_findings = ["java.lang.ExceptionInInitializerError"], target_class = "com.example.ObjectInputStreamDeserialization", ) @@ -13,10 +15,22 @@ java_fuzz_target_test( srcs = [ "ReflectiveCall.java", ], + expected_findings = ["java.lang.ExceptionInInitializerError"], target_class = "com.example.ReflectiveCall", ) java_fuzz_target_test( + name = "LibraryLoad", + srcs = [ + "LibraryLoad.java", + ], + target_class = "com.example.LibraryLoad", + # loading of native libraries is very slow on macos, + # especially using Java 17 + target_compatible_with = SKIP_ON_MACOS, +) + +java_fuzz_target_test( name = "ExpressionLanguageInjection", srcs = [ "ExpressionLanguageInjection.java", @@ -27,6 +41,100 @@ java_fuzz_target_test( "@maven//:javax_el_javax_el_api", "@maven//:javax_validation_validation_api", "@maven//:javax_xml_bind_jaxb_api", + "@maven//:org_glassfish_javax_el", "@maven//:org_hibernate_hibernate_validator", ], ) + +java_fuzz_target_test( + name = "OsCommandInjectionProcessBuilder", + srcs = [ + "OsCommandInjectionProcessBuilder.java", + ], + target_class = "com.example.OsCommandInjectionProcessBuilder", +) + +java_fuzz_target_test( + name = "OsCommandInjectionRuntimeExec", + srcs = [ + "OsCommandInjectionRuntimeExec.java", + ], + target_class = "com.example.OsCommandInjectionRuntimeExec", +) + +java_fuzz_target_test( + name = "LdapSearchInjection", + srcs = [ + "LdapSearchInjection.java", + "ldap/MockInitialContextFactory.java", + "ldap/MockLdapContext.java", + ], + expected_findings = ["javax.naming.directory.InvalidSearchFilterException"], + target_class = "com.example.LdapSearchInjection", + deps = [ + "@maven//:com_unboundid_unboundid_ldapsdk", + ], +) + +java_fuzz_target_test( + name = "LdapDnInjection", + srcs = [ + "LdapDnInjection.java", + "ldap/MockInitialContextFactory.java", + "ldap/MockLdapContext.java", + ], + expected_findings = ["javax.naming.NamingException"], + target_class = "com.example.LdapDnInjection", + deps = [ + "@maven//:com_unboundid_unboundid_ldapsdk", + ], +) + +java_fuzz_target_test( + name = "RegexInsecureQuoteInjection", + srcs = ["RegexInsecureQuoteInjection.java"], + target_class = "com.example.RegexInsecureQuoteInjection", +) + +java_fuzz_target_test( + name = "RegexCanonEqInjection", + srcs = [ + "RegexCanonEqInjection.java", + ], + target_class = "com.example.RegexCanonEqInjection", +) + +java_fuzz_target_test( + name = "ClassLoaderLoadClass", + srcs = [ + "ClassLoaderLoadClass.java", + ], + expected_findings = ["java.lang.ExceptionInInitializerError"], + target_class = "com.example.ClassLoaderLoadClass", +) + +java_fuzz_target_test( + name = "RegexRoadblocks", + srcs = ["RegexRoadblocks.java"], + fuzzer_args = [ + # Limit the number of runs to verify that the regex roadblocks are + # cleared quickly. + "-runs=22000", + ], + target_class = "com.example.RegexRoadblocks", +) + +java_fuzz_target_test( + name = "SqlInjection", + srcs = [ + "SqlInjection.java", + ], + expected_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + "org.h2.jdbc.JdbcSQLSyntaxErrorException", + ], + target_class = "com.example.SqlInjection", + deps = [ + "@maven//:com_h2database_h2", + ], +) diff --git a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java new file mode 100644 index 00000000..c3fa47ac --- /dev/null +++ b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java @@ -0,0 +1,30 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.lang.reflect.InvocationTargetException; + +public class ClassLoaderLoadClass { + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws InterruptedException { + String input = data.consumeRemainingAsAsciiString(); + try { + // create an instance to trigger class initialization + ClassLoaderLoadClass.class.getClassLoader().loadClass(input).getConstructor().newInstance(); + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException + | IllegalAccessException | NoSuchMethodException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/LdapDnInjection.java b/sanitizers/src/test/java/com/example/LdapDnInjection.java new file mode 100644 index 00000000..911db1dc --- /dev/null +++ b/sanitizers/src/test/java/com/example/LdapDnInjection.java @@ -0,0 +1,39 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; + +public class LdapDnInjection { + private static InitialDirContext ctx; + + public static void fuzzerInitialize() throws NamingException { + Hashtable<String, String> env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory"); + ctx = new InitialDirContext(env); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { + // Externally provided DN input needs to be escaped properly + String ou = fuzzedDataProvider.consumeRemainingAsString(); + String base = "ou=" + ou + ",dc=example,dc=com"; + ctx.search(base, "(&(uid=foo)(cn=bar))", new SearchControls()); + } +} diff --git a/sanitizers/src/test/java/com/example/LdapSearchInjection.java b/sanitizers/src/test/java/com/example/LdapSearchInjection.java new file mode 100644 index 00000000..b3dfee74 --- /dev/null +++ b/sanitizers/src/test/java/com/example/LdapSearchInjection.java @@ -0,0 +1,39 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.SearchControls; +import javax.naming.ldap.InitialLdapContext; + +public class LdapSearchInjection { + private static InitialLdapContext ctx; + + public static void fuzzerInitialize() throws NamingException { + Hashtable<String, String> env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory"); + ctx = new InitialLdapContext(env, null); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { + // Externally provided LDAP query input needs to be escaped properly + String username = fuzzedDataProvider.consumeRemainingAsAsciiString(); + String filter = "(&(uid=" + username + ")(ou=security))"; + ctx.search("dc=example,dc=com", filter, new SearchControls()); + } +} diff --git a/driver/testdata/test/FuzzTargetWithInit.java b/sanitizers/src/test/java/com/example/LibraryLoad.java index 86aed82b..81411767 100644 --- a/driver/testdata/test/FuzzTargetWithInit.java +++ b/sanitizers/src/test/java/com/example/LibraryLoad.java @@ -12,19 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package test; +package com.example; -class FuzzTargetWithInit { - static String[] crashOnString; - public static void fuzzerInitialize(String[] args) { - crashOnString = args; - } - public static void fuzzerTestOneInput(byte[] input) { - String inputString = new String(input); - for (String crashString : crashOnString) { - if (inputString.equals(crashString)) { - throw new RuntimeException("triggered the exception"); - } +import com.code_intelligence.jazzer.api.FuzzedDataProvider; + +public class LibraryLoad { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + + try { + System.loadLibrary(input); + } catch (SecurityException | UnsatisfiedLinkError | NullPointerException + | IllegalArgumentException ignored) { } } } diff --git a/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java b/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java new file mode 100644 index 00000000..f5d52782 --- /dev/null +++ b/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java @@ -0,0 +1,35 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.concurrent.TimeUnit; + +public class OsCommandInjectionProcessBuilder { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + try { + ProcessBuilder processBuilder = new ProcessBuilder(input); + processBuilder.environment().clear(); + Process process = processBuilder.start(); + // This should be way faster, but we have to wait until the call is done + if (!process.waitFor(10, TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } catch (Exception ignored) { + // Ignore execution and setup exceptions + } + } +} diff --git a/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java b/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java new file mode 100644 index 00000000..c620a751 --- /dev/null +++ b/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java @@ -0,0 +1,35 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import static java.lang.Runtime.getRuntime; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.concurrent.TimeUnit; + +public class OsCommandInjectionRuntimeExec { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + try { + Process process = getRuntime().exec(input, new String[] {}); + // This should be way faster, but we have to wait until the call is done + if (!process.waitFor(10, TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } catch (Exception ignored) { + // Ignore execution and setup exceptions + } + } +} diff --git a/sanitizers/src/test/java/com/example/ReflectiveCall.java b/sanitizers/src/test/java/com/example/ReflectiveCall.java index 7f85e486..e6b62b45 100644 --- a/sanitizers/src/test/java/com/example/ReflectiveCall.java +++ b/sanitizers/src/test/java/com/example/ReflectiveCall.java @@ -15,7 +15,6 @@ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.lang.reflect.InvocationTargetException; public class ReflectiveCall { public static void fuzzerTestOneInput(FuzzedDataProvider data) { @@ -23,9 +22,8 @@ public class ReflectiveCall { if (input.startsWith("@")) { String className = input.substring(1); try { - Class.forName(className).getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException - | NoSuchMethodException | ClassNotFoundException ignored) { + Class.forName(className); + } catch (ClassNotFoundException ignored) { } } } diff --git a/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java b/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java new file mode 100644 index 00000000..e2d0b722 --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java @@ -0,0 +1,41 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class RegexCanonEqInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsString(); + try { + Pattern.compile(Pattern.quote(input), Pattern.CANON_EQ); + } catch (PatternSyntaxException ignored) { + } catch (IllegalArgumentException ignored) { + // "[媼" generates an IllegalArgumentException but only on Windows using + // Java 8. We ignore this for now. + // + // java.lang.IllegalArgumentException + // at java.lang.AbstractStringBuilder.appendCodePoint(AbstractStringBuilder.java:800) + // at java.lang.StringBuilder.appendCodePoint(StringBuilder.java:240) + // at java.util.regex.Pattern.normalizeCharClass(Pattern.java:1430) + // at java.util.regex.Pattern.normalize(Pattern.java:1396) + // at java.util.regex.Pattern.compile(Pattern.java:1665) + // at java.util.regex.Pattern.<init>(Pattern.java:1352) + // at java.util.regex.Pattern.compile(Pattern.java:1054) + } + } +} diff --git a/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java b/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java new file mode 100644 index 00000000..a548cfb2 --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java @@ -0,0 +1,29 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class RegexInsecureQuoteInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsString(); + try { + Pattern.matches("\\Q" + input + "\\E", "foobar"); + } catch (PatternSyntaxException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/RegexRoadblocks.java b/sanitizers/src/test/java/com/example/RegexRoadblocks.java new file mode 100644 index 00000000..21986e3d --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexRoadblocks.java @@ -0,0 +1,89 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import java.util.regex.Pattern; + +public class RegexRoadblocks { + // We accept arbitrary suffixes but not prefixes for the following reasons: + // 1. The fuzzer will take much longer to match the exact length of the input than to satisfy the + // compare checks, which is what we really want to test. + // 2. Accepting arbitrary prefixes could lead to tests passing purely due to ToC entries being + // emitted in arbitrary positions, but we want to ensure that compares are correctly reported + // including position hints. + private static final Pattern LITERAL = Pattern.compile("foobarbaz.*"); + private static final Pattern QUOTED_LITERAL = Pattern.compile(Pattern.quote("jazzer_is_cool.*")); + private static final Pattern CASE_INSENSITIVE_LITERAL = + Pattern.compile("JaZzER!.*", Pattern.CASE_INSENSITIVE); + private static final Pattern GROUP = Pattern.compile("(always).*"); + private static final Pattern ALTERNATIVE = Pattern.compile("(to_be|not_to_be).*"); + private static final Pattern SINGLE_LATIN1_CHAR_PROPERTY = Pattern.compile("[€].*"); + private static final Pattern MULTIPLE_LATIN1_CHAR_PROPERTY = Pattern.compile("[ẞÄ].*"); + private static final Pattern RANGE_LATIN1_CHAR_PROPERTY = Pattern.compile("[¢-¥].*"); + + private static int run = 0; + + private static boolean matchedLiteral = false; + private static boolean matchedQuotedLiteral = false; + private static boolean matchedCaseInsensitiveLiteral = false; + private static boolean matchedGroup = false; + private static boolean matchedAlternative = false; + private static boolean matchedSingleLatin1CharProperty = false; + private static boolean matchedMultipleLatin1CharProperty = false; + private static boolean matchedRangeLatin1CharProperty = false; + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + run++; + String input = data.consumeRemainingAsString(); + + if (!matchedLiteral && LITERAL.matcher(input).matches()) { + System.out.println("Cleared LITERAL"); + matchedLiteral = true; + } else if (!matchedQuotedLiteral && QUOTED_LITERAL.matcher(input).matches()) { + System.out.println("Cleared QUOTED_LITERAL"); + matchedQuotedLiteral = true; + } else if (!matchedCaseInsensitiveLiteral + && CASE_INSENSITIVE_LITERAL.matcher(input).matches()) { + System.out.println("Cleared CASE_INSENSITIVE_LITERAL"); + matchedCaseInsensitiveLiteral = true; + } else if (!matchedGroup && GROUP.matcher(input).matches()) { + System.out.println("Cleared GROUP"); + matchedGroup = true; + } else if (!matchedAlternative && ALTERNATIVE.matcher(input).matches()) { + System.out.println("Cleared ALTERNATIVE"); + matchedAlternative = true; + } else if (!matchedSingleLatin1CharProperty + && SINGLE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared SINGLE_LATIN1_CHAR_PROPERTY"); + matchedSingleLatin1CharProperty = true; + } else if (!matchedMultipleLatin1CharProperty + && MULTIPLE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared MULTIPLE_LATIN1_CHAR_PROPERTY"); + matchedMultipleLatin1CharProperty = true; + } else if (!matchedRangeLatin1CharProperty + && RANGE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared RANGE_LATIN1_CHAR_PROPERTY"); + matchedRangeLatin1CharProperty = true; + } + + if (matchedLiteral && matchedQuotedLiteral && matchedCaseInsensitiveLiteral && matchedGroup + && matchedAlternative && matchedSingleLatin1CharProperty + && matchedMultipleLatin1CharProperty && matchedRangeLatin1CharProperty) { + throw new FuzzerSecurityIssueLow("Fuzzer matched all regexes in " + run + " runs"); + } + } +} diff --git a/sanitizers/src/test/java/com/example/SqlInjection.java b/sanitizers/src/test/java/com/example/SqlInjection.java new file mode 100644 index 00000000..8a16b5c8 --- /dev/null +++ b/sanitizers/src/test/java/com/example/SqlInjection.java @@ -0,0 +1,41 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.sql.Connection; +import java.sql.SQLException; +import org.h2.jdbcx.JdbcDataSource; + +public class SqlInjection { + static Connection conn = null; + + public static void fuzzerInitialize() throws Exception { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:./test.db"); + conn = ds.getConnection(); + conn.createStatement().execute( + "CREATE TABLE IF NOT EXISTS pet (id IDENTITY PRIMARY KEY, name VARCHAR(50))"); + } + + static void insecureInsertUser(String userName) throws SQLException { + // Never use String.format instead of java.sql.Connection.prepareStatement ... + conn.createStatement().execute(String.format("INSERT INTO pet (name) VALUES ('%s')", userName)); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + insecureInsertUser(data.consumeRemainingAsString()); + } +} diff --git a/driver/testdata/test/FuzzTargetWithCoverage.java b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java index 599b1fa8..b674f5c5 100644 --- a/driver/testdata/test/FuzzTargetWithCoverage.java +++ b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java @@ -12,17 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package test; +package com.example.ldap; -import com.code_intelligence.jazzer.runtime.CoverageMap; +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; -public class FuzzTargetWithCoverage { - public static void fuzzerTestOneInput(byte[] input) { - // manually increase the first coverage counter - byte counter = CoverageMap.mem.get(0); - counter++; - if (counter == 0) - counter--; - CoverageMap.mem.put(0, counter); +public class MockInitialContextFactory implements InitialContextFactory { + public Context getInitialContext(Hashtable environment) { + return new MockLdapContext(); } } diff --git a/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java b/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java new file mode 100644 index 00000000..a51fadcd --- /dev/null +++ b/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java @@ -0,0 +1,316 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example.ldap; + +import com.unboundid.ldap.sdk.DN; +import com.unboundid.ldap.sdk.Filter; +import com.unboundid.ldap.sdk.LDAPException; +import java.util.Hashtable; +import javax.naming.*; +import javax.naming.directory.*; +import javax.naming.ldap.*; + +/** + * Mock LdapContex implementation to test LdapInjection hook configuration. + * + * Only {@code com.example.ldap.MockLdapContext#search(java.lang.String, java.lang.String, + * javax.naming.directory.SearchControls)} is implemented to validate DN and filer query. + */ +public class MockLdapContext implements LdapContext { + @Override + public ExtendedResponse extendedOperation(ExtendedRequest request) throws NamingException { + return null; + } + + @Override + public LdapContext newInstance(Control[] requestControls) throws NamingException { + return this; + } + + @Override + public void reconnect(Control[] connCtls) throws NamingException {} + + @Override + public Control[] getConnectControls() throws NamingException { + return new Control[0]; + } + + @Override + public void setRequestControls(Control[] requestControls) throws NamingException {} + + @Override + public Control[] getRequestControls() throws NamingException { + return new Control[0]; + } + + @Override + public Control[] getResponseControls() throws NamingException { + return new Control[0]; + } + + @Override + public Attributes getAttributes(Name name) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(String name) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(Name name, String[] attrIds) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(String name, String[] attrIds) throws NamingException { + return null; + } + + @Override + public void modifyAttributes(Name name, int mod_op, Attributes attrs) throws NamingException {} + + @Override + public void modifyAttributes(String name, int mod_op, Attributes attrs) throws NamingException {} + + @Override + public void modifyAttributes(Name name, ModificationItem[] mods) throws NamingException {} + + @Override + public void modifyAttributes(String name, ModificationItem[] mods) throws NamingException {} + + @Override + public void bind(Name name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void bind(String name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void rebind(Name name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void rebind(String name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public DirContext createSubcontext(Name name, Attributes attrs) throws NamingException { + return this; + } + + @Override + public DirContext createSubcontext(String name, Attributes attrs) throws NamingException { + return this; + } + + @Override + public DirContext getSchema(Name name) throws NamingException { + return this; + } + + @Override + public DirContext getSchema(String name) throws NamingException { + return this; + } + + @Override + public DirContext getSchemaClassDefinition(Name name) throws NamingException { + return this; + } + + @Override + public DirContext getSchemaClassDefinition(String name) throws NamingException { + return this; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, Attributes matchingAttributes, + String[] attributesToReturn) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, Attributes matchingAttributes, + String[] attributesToReturn) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, Attributes matchingAttributes) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, Attributes matchingAttributes) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, String filter, SearchControls cons) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls cons) + throws NamingException { + // Use UnboundID LDAP to validate DN and filter + if (!DN.isValidDN(name)) { + throw new NamingException("Invalid DN " + name); + } + try { + Filter.create(filter); + } catch (LDAPException e) { + throw new InvalidSearchFilterException("Invalid search filter " + filter); + } + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, String filterExpr, Object[] filterArgs, + SearchControls cons) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, String filterExpr, Object[] filterArgs, + SearchControls cons) throws NamingException { + return null; + } + + @Override + public Object lookup(Name name) throws NamingException { + return this; + } + + @Override + public Object lookup(String name) throws NamingException { + return this; + } + + @Override + public void bind(Name name, Object obj) throws NamingException {} + + @Override + public void bind(String name, Object obj) throws NamingException {} + + @Override + public void rebind(Name name, Object obj) throws NamingException {} + + @Override + public void rebind(String name, Object obj) throws NamingException {} + + @Override + public void unbind(Name name) throws NamingException {} + + @Override + public void unbind(String name) throws NamingException {} + + @Override + public void rename(Name oldName, Name newName) throws NamingException {} + + @Override + public void rename(String oldName, String newName) throws NamingException {} + + @Override + public NamingEnumeration<NameClassPair> list(Name name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<NameClassPair> list(String name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<Binding> listBindings(Name name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<Binding> listBindings(String name) throws NamingException { + return null; + } + + @Override + public void destroySubcontext(Name name) throws NamingException {} + + @Override + public void destroySubcontext(String name) throws NamingException {} + + @Override + public Context createSubcontext(Name name) throws NamingException { + return this; + } + + @Override + public Context createSubcontext(String name) throws NamingException { + return this; + } + + @Override + public Object lookupLink(Name name) throws NamingException { + return this; + } + + @Override + public Object lookupLink(String name) throws NamingException { + return this; + } + + @Override + public NameParser getNameParser(Name name) throws NamingException { + return null; + } + + @Override + public NameParser getNameParser(String name) throws NamingException { + return null; + } + + @Override + public Name composeName(Name name, Name prefix) throws NamingException { + return null; + } + + @Override + public String composeName(String name, String prefix) throws NamingException { + return null; + } + + @Override + public Object addToEnvironment(String propName, Object propVal) throws NamingException { + return null; + } + + @Override + public Object removeFromEnvironment(String propName) throws NamingException { + return null; + } + + @Override + public Hashtable<?, ?> getEnvironment() throws NamingException { + return null; + } + + @Override + public void close() throws NamingException {} + + @Override + public String getNameInNamespace() throws NamingException { + return null; + } +} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel new file mode 100644 index 00000000..cbc77434 --- /dev/null +++ b/tests/BUILD.bazel @@ -0,0 +1,259 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "java_jni_library") +load("//bazel:compat.bzl", "SKIP_ON_MACOS", "SKIP_ON_WINDOWS") +load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") + +java_fuzz_target_test( + name = "LongStringFuzzer", + srcs = [ + "src/test/java/com/example/LongStringFuzzer.java", + ], + data = ["src/test/java/com/example/LongStringFuzzerInput"], + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = [ + "$(rootpath src/test/java/com/example/LongStringFuzzerInput)", + ], + target_class = "com.example.LongStringFuzzer", + verify_crash_input = False, +) + +java_fuzz_target_test( + name = "JpegImageParserAutofuzz", + expected_findings = ["java.lang.NegativeArraySizeException"], + fuzzer_args = [ + "--autofuzz=org.apache.commons.imaging.formats.jpeg.JpegImageParser::getBufferedImage", + # Exit after the first finding for testing purposes. + "--keep_going=1", + "--autofuzz_ignore=java.lang.NullPointerException", + ], + runtime_deps = [ + "@maven//:org_apache_commons_commons_imaging", + ], +) + +java_fuzz_target_test( + name = "HookDependenciesFuzzer", + srcs = ["src/test/java/com/example/HookDependenciesFuzzer.java"], + env = {"JAVA_OPTS": "-Xverify:all"}, + hook_classes = ["com.example.HookDependenciesFuzzer"], + target_class = "com.example.HookDependenciesFuzzer", +) + +java_fuzz_target_test( + name = "AutofuzzWithoutCoverage", + expected_findings = ["java.lang.NullPointerException"], + fuzzer_args = [ + # Autofuzz a method that triggers no coverage instrumentation (the Java standard library is + # excluded by default). + "--autofuzz=java.util.regex.Pattern::compile", + "--keep_going=1", + ], +) + +java_fuzz_target_test( + name = "AutofuzzHookDependencies", + # The reproducer does not include the hook on OOM and thus throws a regular error. + expected_findings = ["java.lang.OutOfMemoryError"], + fuzzer_args = [ + "--instrumentation_includes=java.util.regex.**", + "--autofuzz=java.util.regex.Pattern::compile", + "--autofuzz_ignore=java.lang.Exception", + "--keep_going=1", + ], + # FIXME(fabian): Regularly times out on Windows with 0 exec/s for minutes. + target_compatible_with = SKIP_ON_WINDOWS, +) + +java_fuzz_target_test( + name = "ForkModeFuzzer", + size = "enormous", + srcs = [ + "src/test/java/com/example/ForkModeFuzzer.java", + ], + env = { + "JAVA_OPTS": "-Dfoo=not_foo -Djava_opts=1", + }, + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = [ + "-fork=2", + "--additional_jvm_args=-Dbaz=baz", + ] + select({ + # \\\\ becomes \\ when evaluated as a Starlark string literal, then \ in + # java_fuzz_target_test. + "@platforms//os:windows": ["--jvm_args=-Dfoo=foo;-Dbar=b\\\\;ar"], + "//conditions:default": ["--jvm_args=-Dfoo=foo:-Dbar=b\\\\:ar"], + }), + # Consumes more resources than can be expressed via the size attribute. + tags = ["exclusive-if-local"], + target_class = "com.example.ForkModeFuzzer", + # The exit codes of the forked libFuzzer processes are not picked up correctly. + target_compatible_with = SKIP_ON_MACOS, +) + +java_fuzz_target_test( + name = "CoverageFuzzer", + srcs = [ + "src/test/java/com/example/CoverageFuzzer.java", + ], + env = { + "COVERAGE_REPORT_FILE": "coverage.txt", + "COVERAGE_DUMP_FILE": "coverage.exec", + }, + fuzzer_args = [ + "-use_value_profile=1", + "--coverage_report=coverage.txt", + "--coverage_dump=coverage.exec", + "--instrumentation_includes=com.example.**", + ], + target_class = "com.example.CoverageFuzzer", + verify_crash_input = False, + verify_crash_reproducer = False, + deps = [ + "@jazzer_jacoco//:jacoco_internal", + ], +) + +java_library( + name = "autofuzz_inner_class_target", + srcs = ["src/test/java/com/example/AutofuzzInnerClassTarget.java"], + deps = [ + "//agent:jazzer_api_compile_only", + ], +) + +java_fuzz_target_test( + name = "AutofuzzInnerClassFuzzer", + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = [ + "--autofuzz=com.example.AutofuzzInnerClassTarget.Middle.Inner::test", + "--keep_going=1", + ], + runtime_deps = [ + ":autofuzz_inner_class_target", + ], +) + +# Regression test for https://github.com/CodeIntelligenceTesting/jazzer/issues/405. +java_fuzz_target_test( + name = "MemoryLeakFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/MemoryLeakFuzzer.java"], + env = { + "JAVA_OPTS": "-Xmx800m", + }, + expect_crash = False, + fuzzer_args = [ + # Before the bug was fixed, either the GC overhead limit or the overall heap limit was + # reached by this target in this number of runs. + "-runs=1000000", + # Skip over the first and only exception to keep the fuzzer running until it hits the runs + # limit. + "--keep_going=2", + ], + target_class = "com.example.MemoryLeakFuzzer", +) + +JAZZER_API_TEST_CASES = { + "default": [], + "nohooks": ["--nohooks"], +} + +[ + java_fuzz_target_test( + name = "JazzerApiFuzzer_" + case, + srcs = ["src/test/java/com/example/JazzerApiFuzzer.java"], + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = args, + target_class = "com.example.JazzerApiFuzzer", + ) + for case, args in JAZZER_API_TEST_CASES.items() +] + +java_fuzz_target_test( + name = "DisabledHooksFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/DisabledHooksFuzzer.java"], + expect_crash = False, + fuzzer_args = [ + "-runs=0", + "--custom_hooks=com.example.DisabledHook", + ] + select({ + "@platforms//os:windows": ["--disabled_hooks=com.example.DisabledHook;com.code_intelligence.jazzer.sanitizers.RegexInjection"], + "//conditions:default": ["--disabled_hooks=com.example.DisabledHook:com.code_intelligence.jazzer.sanitizers.RegexInjection"], + }), + target_class = "com.example.DisabledHooksFuzzer", +) + +java_fuzz_target_test( + name = "BytesMemoryLeakFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/BytesMemoryLeakFuzzer.java"], + env = { + "JAVA_OPTS": "-Xmx200m", + }, + expect_crash = False, + fuzzer_args = [ + # Before the bug was fixed, either the GC overhead limit or the overall heap limit was + # reached by this target in this number of runs. + "-runs=10000000", + ], + target_class = "com.example.BytesMemoryLeakFuzzer", +) + +# Verifies that Jazzer continues fuzzing when the first two executions did not result in any +# coverage feedback. +java_fuzz_target_test( + name = "NoCoverageFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/NoCoverageFuzzer.java"], + expect_crash = False, + fuzzer_args = [ + "-runs=10", + "--instrumentation_excludes=**", + ], + target_class = "com.example.NoCoverageFuzzer", +) + +java_fuzz_target_test( + name = "SeedFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/SeedFuzzer.java"], + expect_crash = False, + fuzzer_args = [ + "-runs=0", + "-seed=1234567", + ], + target_class = "com.example.SeedFuzzer", +) + +java_fuzz_target_test( + name = "NoSeedFuzzer", + timeout = "short", + srcs = ["src/test/java/com/example/NoSeedFuzzer.java"], + env = { + "JAZZER_NO_EXPLICIT_SEED": "1", + }, + expect_crash = False, + fuzzer_args = [ + "-runs=0", + ], + target_class = "com.example.NoSeedFuzzer", +) + +java_jni_library( + name = "native_value_profile_fuzzer", + srcs = ["src/test/java/com/example/NativeValueProfileFuzzer.java"], + native_libs = ["//tests/src/test/native/com/example:native_value_profile_fuzzer"], + visibility = ["//tests/src/test/native/com/example:__pkg__"], + deps = ["//agent:jazzer_api_compile_only"], +) + +java_fuzz_target_test( + name = "NativeValueProfileFuzzer", + expected_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = ["-use_value_profile=1"], + sanitizer = "address", + target_class = "com.example.NativeValueProfileFuzzer", + target_compatible_with = SKIP_ON_WINDOWS, + verify_crash_reproducer = False, + runtime_deps = [":native_value_profile_fuzzer"], +) diff --git a/tests/src/test/java/com/example/AutofuzzInnerClassTarget.java b/tests/src/test/java/com/example/AutofuzzInnerClassTarget.java new file mode 100644 index 00000000..16240eff --- /dev/null +++ b/tests/src/test/java/com/example/AutofuzzInnerClassTarget.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; + +@SuppressWarnings("unused") +public class AutofuzzInnerClassTarget { + public static class Middle { + public static class Inner { + public void test(int a, int b) { + if (a == b) { + throw new FuzzerSecurityIssueLow("Finished Autofuzz Target"); + } + } + } + } +} diff --git a/driver/signal_handler.h b/tests/src/test/java/com/example/BytesMemoryLeakFuzzer.java index d0d17121..95406316 100644 --- a/driver/signal_handler.h +++ b/tests/src/test/java/com/example/BytesMemoryLeakFuzzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Code Intelligence GmbH + * Copyright 2022 Code Intelligence GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,8 @@ * limitations under the License. */ -#pragma once +package com.example; -#include <jni.h> - -namespace jazzer { -// SignalHandler registers handlers for signals (e.g. SIGINT) in Java and -// notifies the driver via native callbacks when the handlers fire. -class SignalHandler { - public: - // Set up handlers for signal in Java. - static void Setup(JNIEnv &env); -}; -} // namespace jazzer +public class BytesMemoryLeakFuzzer { + public static void fuzzerTestOneInput(byte[] data) {} +} diff --git a/tests/src/test/java/com/example/CoverageFuzzer.java b/tests/src/test/java/com/example/CoverageFuzzer.java new file mode 100644 index 00000000..8f63639d --- /dev/null +++ b/tests/src/test/java/com/example/CoverageFuzzer.java @@ -0,0 +1,198 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionData; +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataReader; +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.ExecutionDataStore; +import com.code_intelligence.jazzer.third_party.org.jacoco.core.data.SessionInfoStore; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Test of coverage report and dump. + * + * Internally, JaCoCo is used to gather coverage information to guide the fuzzer to cover new + * branches. This information can be dumped in the JaCoCo format and used to generate reports later + * on. The dump only contains classes with at least one coverage data point. A JaCoCo report will + * also include completely uncovered files based on the available classes in the stated jar files + * in the report command. + * + * A human-readable coverage report can be generated directly by Jazzer. It contains information + * on file level about all classes that should have been instrumented according to the + * instrumentation_includes and instrumentation_exclude filters. + */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public final class CoverageFuzzer { + // Not used during fuzz run, so not included in the dump + public static class ClassNotToCover { + private final int i; + public ClassNotToCover(int i) { + this.i = i; + } + public int getI() { + return i; + } + } + + // Used in the fuzz run and included in the dump + public static class ClassToCover { + private final int i; + + public ClassToCover(int i) { + if (i < 0 || i > 1000) { + throw new IllegalArgumentException(String.format("Invalid repeat number \"%d\"", i)); + } + this.i = i; + } + + public String repeat(String str) { + if (str != null && str.length() >= 3 && str.length() <= 10) { + return IntStream.range(0, i).mapToObj(i -> str).collect(Collectors.joining()); + } + throw new IllegalArgumentException(String.format("Invalid str \"%s\"", str)); + } + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + try { + ClassToCover classToCover = new ClassToCover(data.consumeInt()); + String repeated = classToCover.repeat(data.consumeRemainingAsAsciiString()); + if (repeated.equals("foofoofoo")) { + throw new FuzzerSecurityIssueLow("Finished coverage fuzzer test"); + } + } catch (IllegalArgumentException ignored) { + } + } + + public static void fuzzerTearDown() throws IOException { + assertCoverageReport(); + assertCoverageDump(); + } + + private static void assertCoverageReport() throws IOException { + List<String> coverage = Files.readAllLines(Paths.get(System.getenv("COVERAGE_REPORT_FILE"))); + List<List<String>> sections = new ArrayList<>(4); + sections.add(new ArrayList<>()); + coverage.forEach(l -> { + if (l.isEmpty()) { + sections.add(new ArrayList<>()); + } else { + sections.get(sections.size() - 1).add(l); + } + }); + + List<String> branchCoverage = sections.get(0); + assertEquals(2, branchCoverage.size()); + List<String> lineCoverage = sections.get(1); + assertEquals(2, lineCoverage.size()); + List<String> incompleteCoverage = sections.get(2); + assertEquals(2, incompleteCoverage.size()); + List<String> missedCoverage = sections.get(3); + assertEquals(2, missedCoverage.size()); + + assertNotNull( + branchCoverage.stream() + .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find branch coverage"))); + + assertNotNull( + lineCoverage.stream() + .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find line coverage"))); + + assertNotNull( + incompleteCoverage.stream() + .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find incomplete coverage"))); + + String missed = + missedCoverage.stream() + .filter(l -> l.startsWith(CoverageFuzzer.class.getSimpleName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find missed coverage")); + List<String> missingLines = IntStream.rangeClosed(63, 79) + .mapToObj(i -> " " + i) + .filter(missed::contains) + .collect(Collectors.toList()); + if (!missingLines.isEmpty()) { + throw new IllegalStateException(String.format( + "Missing coverage for ClassToCover on lines %s", String.join(", ", missingLines))); + } + } + + private static void assertCoverageDump() throws IOException { + ExecutionDataStore executionDataStore = new ExecutionDataStore(); + SessionInfoStore sessionInfoStore = new SessionInfoStore(); + try (FileInputStream bais = new FileInputStream(System.getenv("COVERAGE_DUMP_FILE"))) { + ExecutionDataReader reader = new ExecutionDataReader(bais); + reader.setExecutionDataVisitor(executionDataStore); + reader.setSessionInfoVisitor(sessionInfoStore); + reader.read(); + } + assertEquals(2, executionDataStore.getContents().size()); + + ExecutionData coverageFuzzerCoverage = new ExecutionData(0, "", 0); + ExecutionData classToCoverCoverage = new ExecutionData(0, "", 0); + for (ExecutionData content : executionDataStore.getContents()) { + if (content.getName().endsWith("ClassToCover")) { + classToCoverCoverage = content; + } else { + coverageFuzzerCoverage = content; + } + } + + assertEquals("com/example/CoverageFuzzer", coverageFuzzerCoverage.getName()); + assertEquals(7, countHits(coverageFuzzerCoverage.getProbes())); + + assertEquals("com/example/CoverageFuzzer$ClassToCover", classToCoverCoverage.getName()); + assertEquals(11, countHits(classToCoverCoverage.getProbes())); + } + + private static int countHits(boolean[] probes) { + int count = 0; + for (boolean probe : probes) { + if (probe) + count++; + } + return count; + } + + private static <T> void assertEquals(T expected, T actual) { + if (!expected.equals(actual)) { + throw new IllegalStateException( + String.format("Expected \"%s\", got \"%s\"", expected, actual)); + } + } + + private static <T> void assertNotNull(T actual) { + if (actual == null) { + throw new IllegalStateException("Expected none null value, got null"); + } + } +} diff --git a/tests/src/test/java/com/example/DisabledHooksFuzzer.java b/tests/src/test/java/com/example/DisabledHooksFuzzer.java new file mode 100644 index 00000000..430bfa40 --- /dev/null +++ b/tests/src/test/java/com/example/DisabledHooksFuzzer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class DisabledHooksFuzzer { + public static void fuzzerTestOneInput(byte[] data) { + triggerCustomHook(); + triggerBuiltinHook(); + } + + private static void triggerCustomHook() {} + + private static void triggerBuiltinHook() { + // Trigger the built-in regex injection detector if it is enabled, but catch the exception + // thrown if it isn't. + try { + Pattern.compile("["); + } catch (PatternSyntaxException ignored) { + } + } +} + +class DisabledHook { + @MethodHook(type = HookType.BEFORE, targetClassName = "com.example.DisabledHooksFuzzer", + targetMethod = "triggerCustomHook", targetMethodDescriptor = "()V") + public static void + triggerCustomHookHook(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + Jazzer.reportFindingFromHook( + new IllegalStateException("hook on triggerCustomHook should have been disabled")); + } +} diff --git a/tests/src/test/java/com/example/ForkModeFuzzer.java b/tests/src/test/java/com/example/ForkModeFuzzer.java new file mode 100644 index 00000000..9f005124 --- /dev/null +++ b/tests/src/test/java/com/example/ForkModeFuzzer.java @@ -0,0 +1,48 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; + +public final class ForkModeFuzzer { + public static void fuzzerInitialize() { + // When running through a Java reproducer, do not check the Java opts. + if (System.getProperty("jazzer.is_reproducer") != null) + return; + String foo = System.getProperty("foo"); + String bar = System.getProperty("bar"); + String baz = System.getProperty("baz"); + // Only used to verify that arguments are correctly passed down to child processes. + if (foo == null || bar == null || baz == null || !foo.equals("foo") + || !(bar.equals("b;ar") || bar.equals("b:ar")) || !baz.equals("baz")) { + // Exit the process with an exit code different from that for a finding. + System.err.println("ERROR: Did not correctly pass all jvm_args to child process."); + System.err.printf("foo: %s%nbar: %s%nbaz: %s%n", foo, bar, baz); + System.exit(3); + } + // Only used to verify that Jazzer honors the JAVA_OPTS env var. + String javaOpts = System.getProperty("java_opts"); + if (javaOpts == null || !javaOpts.equals("1")) { + // Exit the process with an exit code different from that for a finding. + System.err.println("ERROR: Did not honor JAVA_OPTS."); + System.err.printf("java_opts: %s%n", javaOpts); + System.exit(4); + } + } + + public static void fuzzerTestOneInput(byte[] data) { + throw new FuzzerSecurityIssueLow("Passed fuzzerInitialize"); + } +} diff --git a/tests/src/test/java/com/example/HookDependenciesFuzzer.java b/tests/src/test/java/com/example/HookDependenciesFuzzer.java new file mode 100644 index 00000000..88627f4c --- /dev/null +++ b/tests/src/test/java/com/example/HookDependenciesFuzzer.java @@ -0,0 +1,68 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Field; +import java.util.regex.Pattern; + +// This fuzzer verifies that: +// 1. a class referenced in a static initializer of a hook is still instrumented with the hook; +// 2. hooks that are not shipped in the Jazzer agent JAR can still instrument Java standard library +// classes. +public class HookDependenciesFuzzer { + private static final Field PATTERN_ROOT; + + static { + Field root; + try { + root = Pattern.class.getDeclaredField("root"); + } catch (NoSuchFieldException e) { + root = null; + } + PATTERN_ROOT = root; + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Matcher", + targetMethod = "matches", targetMethodDescriptor = "()Z", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + matcherMatchesHook(MethodHandle method, Object alwaysNull, Object[] alwaysEmpty, int hookId, + Boolean returnValue) { + if (PATTERN_ROOT != null) { + throw new FuzzerSecurityIssueLow("Hook applied even though it depends on the class to hook"); + } + } + + public static void fuzzerTestOneInput(byte[] data) { + try { + Pattern.matches("foobar", "foobar"); + } catch (Throwable t) { + if (t instanceof FuzzerSecurityIssueLow) { + throw t; + } else { + // Unexpected exception, exit without producing a finding to let the test fail due to the + // missing Java reproducer. + // FIXME(fabian): This is hacky and will result in false positives as soon as we implement + // Java reproducers for fuzz target exits. Replace this with a more reliable signal. + t.printStackTrace(); + System.exit(1); + } + } + } +} diff --git a/tests/src/test/java/com/example/JazzerApiFuzzer.java b/tests/src/test/java/com/example/JazzerApiFuzzer.java new file mode 100644 index 00000000..2428d21f --- /dev/null +++ b/tests/src/test/java/com/example/JazzerApiFuzzer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.api.Jazzer; + +public class JazzerApiFuzzer { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + Jazzer.exploreState(data.consumeByte(), 1); + Jazzer.guideTowardsEquality(data.consumeString(10), data.pickValue(new String[] {"foo"}), 1); + Jazzer.guideTowardsEquality(data.consumeBytes(10), new byte[] {}, 2); + Jazzer.guideTowardsContainment(data.consumeAsciiString(10), "bar", 2); + throw new FuzzerSecurityIssueLow("Jazzer API calls succeed"); + } +} diff --git a/tests/src/test/java/com/example/LongStringFuzzer.java b/tests/src/test/java/com/example/LongStringFuzzer.java new file mode 100644 index 00000000..324764d4 --- /dev/null +++ b/tests/src/test/java/com/example/LongStringFuzzer.java @@ -0,0 +1,32 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; + +/** + * Provoke a finding with huge captured data to verify that the generated crash reproducer is still + * compilable. This test uses a huge, predefined corpus to speed up finding the issue. + * <p> + * Reproduces issue #269 (<a + * href="https://github.com/CodeIntelligenceTesting/jazzer/issues/269">...</a>) + */ +public class LongStringFuzzer { + public static void fuzzerTestOneInput(byte[] data) { + if (data.length > 1024 * 64) { + throw new FuzzerSecurityIssueLow("String too long exception"); + } + } +} diff --git a/tests/src/test/java/com/example/LongStringFuzzerInput b/tests/src/test/java/com/example/LongStringFuzzerInput Binary files differnew file mode 100644 index 00000000..f18c9a67 --- /dev/null +++ b/tests/src/test/java/com/example/LongStringFuzzerInput diff --git a/driver/utils.h b/tests/src/test/java/com/example/MemoryLeakFuzzer.java index 99d7b60e..9f38a1e2 100644 --- a/driver/utils.h +++ b/tests/src/test/java/com/example/MemoryLeakFuzzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Code Intelligence GmbH + * Copyright 2022 Code Intelligence GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,13 @@ * limitations under the License. */ -#pragma once +package com.example; -#include <cstdint> -#include <cstring> -#include <string> +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; -namespace jazzer { - -#if defined(_WIN32) || defined(_WIN64) -constexpr auto kPathSeparator = '\\'; -#else -constexpr auto kPathSeparator = '/'; -#endif - -std::string Sha1Hash(const uint8_t *data, size_t size); -} // namespace jazzer +public class MemoryLeakFuzzer { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + throw new FuzzerSecurityIssueLow(); + } +} diff --git a/tests/src/test/java/com/example/NativeValueProfileFuzzer.java b/tests/src/test/java/com/example/NativeValueProfileFuzzer.java new file mode 100644 index 00000000..1085a953 --- /dev/null +++ b/tests/src/test/java/com/example/NativeValueProfileFuzzer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.github.fmeum.rules_jni.RulesJni; + +public class NativeValueProfileFuzzer { + public static void fuzzerInitialize() { + RulesJni.loadLibrary("native_value_profile_fuzzer", NativeValueProfileFuzzer.class); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + long[] blocks = data.consumeLongs(2); + if (blocks.length != 2) + return; + if (checkAccess(blocks[0], blocks[1])) { + throw new FuzzerSecurityIssueLow("Security breached"); + } + } + + private static native boolean checkAccess(long block1, long block2); +} diff --git a/tests/src/test/java/com/example/NoCoverageFuzzer.java b/tests/src/test/java/com/example/NoCoverageFuzzer.java new file mode 100644 index 00000000..a1f8b4ea --- /dev/null +++ b/tests/src/test/java/com/example/NoCoverageFuzzer.java @@ -0,0 +1,19 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +public class NoCoverageFuzzer { + public static void fuzzerTestOneInput(byte[] data) {} +} diff --git a/tests/src/test/java/com/example/NoSeedFuzzer.java b/tests/src/test/java/com/example/NoSeedFuzzer.java new file mode 100644 index 00000000..bf1c1103 --- /dev/null +++ b/tests/src/test/java/com/example/NoSeedFuzzer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.Jazzer; + +public class NoSeedFuzzer { + public static void fuzzerInitialize() { + // Verify that the seed was randomly generated and not taken to be the fixed + // one set in FuzzTargetTestWrapper. This has a 1 / INT_MAX chance to be + // flaky, which is acceptable. + if (Jazzer.SEED == (int) 2735196724L) { + System.err.println( + "Jazzer.SEED should not equal the fixed seed set in FuzzTargetTestWrapper"); + System.exit(1); + } + } + + public static void fuzzerTestOneInput(byte[] data) {} +} diff --git a/tests/src/test/java/com/example/SeedFuzzer.java b/tests/src/test/java/com/example/SeedFuzzer.java new file mode 100644 index 00000000..4d1e4e8b --- /dev/null +++ b/tests/src/test/java/com/example/SeedFuzzer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import com.code_intelligence.jazzer.api.Jazzer; + +public class SeedFuzzer { + public static void fuzzerInitialize() { + if (Jazzer.SEED != 1234567) { + throw new FuzzerSecurityIssueLow("Expected Jazzer.SEED to be 1234567, got " + Jazzer.SEED); + } + } + + public static void fuzzerTestOneInput(byte[] data) {} +} diff --git a/tests/src/test/native/com/example/BUILD.bazel b/tests/src/test/native/com/example/BUILD.bazel new file mode 100644 index 00000000..93b886a8 --- /dev/null +++ b/tests/src/test/native/com/example/BUILD.bazel @@ -0,0 +1,28 @@ +load("@fmeum_rules_jni//jni:defs.bzl", "cc_jni_library") + +cc_jni_library( + name = "native_value_profile_fuzzer", + srcs = ["native_value_profile_fuzzer.cpp"], + copts = [ + "-fsanitize=fuzzer-no-link,address", + "-fno-sanitize-blacklist", + ], + defines = [ + # Workaround for Windows build failures with VS 2022: + # "lld-link: error: /INFERASANLIBS is not allowed in .drectve" + # https://github.com/llvm/llvm-project/issues/56300#issuecomment-1214313292 + "_DISABLE_STRING_ANNOTATION=1", + "_DISABLE_VECTOR_ANNOTATION=1", + ], + linkopts = select({ + "//:clang_on_linux": ["-fuse-ld=lld"], + "@platforms//os:windows": [ + # Windows requires all symbols that should be imported from the main + # executable to be defined by an import lib. + "/wholearchive:clang_rt.asan_dll_thunk-x86_64.lib", + ], + "//conditions:default": [], + }), + visibility = ["//tests:__pkg__"], + deps = ["//tests:native_value_profile_fuzzer.hdrs"], +) diff --git a/tests/src/test/native/com/example/native_value_profile_fuzzer.cpp b/tests/src/test/native/com/example/native_value_profile_fuzzer.cpp new file mode 100644 index 00000000..2edcc269 --- /dev/null +++ b/tests/src/test/native/com/example/native_value_profile_fuzzer.cpp @@ -0,0 +1,35 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <cstdint> +#include <cstring> + +#include "com_example_NativeValueProfileFuzzer.h" + +// Prevent the compiler from inlining the secret all the way into checkAccess, +// which would make it trivial for the fuzzer to pass the checks. +volatile uint64_t secret = 0xefe4eb93215cb6b0L; + +static uint64_t insecureEncrypt(uint64_t input) { return input ^ secret; } + +jboolean Java_com_example_NativeValueProfileFuzzer_checkAccess(JNIEnv *, jclass, + jlong block1, + jlong block2) { + if (insecureEncrypt(block1) == 0x9fc48ee64d3dc090L) { + if (insecureEncrypt(block2) == 0x888a82ff483ad9c2L) { + return true; + } + } + return false; +} diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel index 0643e4f6..a234e836 100644 --- a/third_party/BUILD.bazel +++ b/third_party/BUILD.bazel @@ -12,3 +12,5 @@ config_setting( }, visibility = ["//visibility:public"], ) + +exports_files(["jacoco_internal.jarjar"]) diff --git a/third_party/asm.BUILD b/third_party/asm.BUILD deleted file mode 100644 index 2f659fc6..00000000 --- a/third_party/asm.BUILD +++ /dev/null @@ -1,32 +0,0 @@ -java_library( - name = "asm", - srcs = glob(["asm/src/main/**/*.java"]), - visibility = ["//visibility:public"], -) - -java_library( - name = "asm_commons", - srcs = glob(["asm-commons/src/main/**/*.java"]), - deps = [ - ":asm", - ":asm_analysis", - ":asm_tree", - ], - visibility = ["//visibility:public"], -) - -java_library( - name = "asm_tree", - srcs = glob(["asm-tree/src/main/**/*.java"]), - deps = [":asm"], - visibility = ["//visibility:public"], -) - -java_library( - name = "asm_analysis", - srcs = glob(["asm-analysis/src/main/**/*.java"]), - deps = [ - ":asm", - ":asm_tree", - ], -) diff --git a/third_party/jacoco-make-probe-inserter-subclassable.patch b/third_party/jacoco-make-probe-inserter-subclassable.patch index 3885fa1f..03cfe5e6 100644 --- a/third_party/jacoco-make-probe-inserter-subclassable.patch +++ b/third_party/jacoco-make-probe-inserter-subclassable.patch @@ -63,21 +63,8 @@ index 00000000..19c2a7e2 + ProbeInserter makeProbeInserter(int access, String name, String desc, + MethodVisitor mv, IProbeArrayStrategy arrayStrategy); +} -diff --git org.jacoco.core/src/org/jacoco/core/internal/instr/InstrSupport.java org.jacoco.core/src/org/jacoco/core/internal/instr/InstrSupport.java -index 71808ac8..3df93f63 100644 ---- org.jacoco.core/src/org/jacoco/core/internal/instr/InstrSupport.java -+++ org.jacoco.core/src/org/jacoco/core/internal/instr/InstrSupport.java -@@ -78,7 +78,7 @@ public final class InstrSupport { - * Data type of the field that stores coverage information for a class ( - * <code>boolean[]</code>). - */ -- public static final String DATAFIELD_DESC = "[Z"; -+ public static final String DATAFIELD_DESC = "java/nio/ByteBuffer"; - - // === Init Method === - diff --git org.jacoco.core/src/org/jacoco/core/internal/instr/ProbeInserter.java org.jacoco.core/src/org/jacoco/core/internal/instr/ProbeInserter.java -index 0f5b99ff..ba5daa6d 100644 +index 0f5b99ff..80965dfe 100644 --- org.jacoco.core/src/org/jacoco/core/internal/instr/ProbeInserter.java +++ org.jacoco.core/src/org/jacoco/core/internal/instr/ProbeInserter.java @@ -25,7 +25,7 @@ import org.objectweb.asm.TypePath; @@ -96,8 +83,8 @@ index 0f5b99ff..ba5daa6d 100644 - private final int variable; + protected final int variable; - /** Maximum stack usage of the code to access the probe array. */ - private int accessorStackSize; + /** Label for the new beginning of the method */ + private final Label beginLabel; @@ -56,7 +56,7 @@ class ProbeInserter extends MethodVisitor implements IProbeInserter { * callback to create the code that retrieves the reference to * the probe array @@ -107,3 +94,55 @@ index 0f5b99ff..ba5daa6d 100644 final MethodVisitor mv, final IProbeArrayStrategy arrayStrategy) { super(InstrSupport.ASM_API_VERSION, mv); this.clinit = InstrSupport.CLINIT_NAME.equals(name); +@@ -91,6 +91,10 @@ class ProbeInserter extends MethodVisitor implements IProbeInserter { + mv.visitInsn(Opcodes.BASTORE); + } + ++ protected Object getLocalVariableType() { ++ return InstrSupport.DATAFIELD_DESC; ++ } ++ + @Override + public void visitCode() { + mv.visitLabel(beginLabel); +@@ -118,6 +122,10 @@ class ProbeInserter extends MethodVisitor implements IProbeInserter { + public AnnotationVisitor visitLocalVariableAnnotation(final int typeRef, + final TypePath typePath, final Label[] start, final Label[] end, + final int[] index, final String descriptor, final boolean visible) { ++ if (getLocalVariableType() == null) { ++ return mv.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible); ++ } ++ + final int[] newIndex = new int[index.length]; + for (int i = 0; i < newIndex.length; i++) { + newIndex[i] = map(index[i]); +@@ -137,6 +145,9 @@ class ProbeInserter extends MethodVisitor implements IProbeInserter { + } + + private int map(final int var) { ++ if (getLocalVariableType() == null) { ++ return var; ++ } + if (var < variable) { + return var; + } else { +@@ -153,13 +164,18 @@ class ProbeInserter extends MethodVisitor implements IProbeInserter { + "ClassReader.accept() should be called with EXPAND_FRAMES flag"); + } + ++ if (getLocalVariableType() == null) { ++ mv.visitFrame(type, nLocal, local, nStack, stack); ++ return; ++ } ++ + final Object[] newLocal = new Object[Math.max(nLocal, variable) + 1]; + int idx = 0; // Arrays index for existing locals + int newIdx = 0; // Array index for new locals + int pos = 0; // Current variable position + while (idx < nLocal || pos <= variable) { + if (pos == variable) { +- newLocal[newIdx++] = InstrSupport.DATAFIELD_DESC; ++ newLocal[newIdx++] = getLocalVariableType(); + pos++; + } else { + if (idx < nLocal) { diff --git a/third_party/jacoco_internal.BUILD b/third_party/jacoco_internal.BUILD index 9e6140a7..38ac7f6c 100644 --- a/third_party/jacoco_internal.BUILD +++ b/third_party/jacoco_internal.BUILD @@ -1,18 +1,37 @@ -java_library( +load("@com_github_johnynek_bazel_jar_jar//:jar_jar.bzl", "jar_jar") + +java_import( name = "jacoco_internal", + jars = ["jacoco_internal_shaded.jar"], + deps = [ + "@org_ow2_asm_asm//jar", + "@org_ow2_asm_asm_commons//jar", + "@org_ow2_asm_asm_tree//jar", + ], + visibility = ["//visibility:public"], +) + +jar_jar( + name = "jacoco_internal_shaded", + input_jar = "libjacoco_internal_unshaded.jar", + rules = "@jazzer//third_party:jacoco_internal.jarjar", +) + +java_library( + name = "jacoco_internal_unshaded", srcs = glob([ "org.jacoco.core/src/org/jacoco/core/**/*.java", ]), resources = glob([ - "org.jacoco.core/src/org/jacoco/core/internal/flow/java_no_throw_methods_list.dat", + "org.jacoco.core/src/org/jacoco/core/**/*.properties", ]), javacopts = [ "-Xep:EqualsHashCode:OFF", + "-Xep:ReturnValueIgnored:OFF", ], deps = [ - "@jazzer_ow2_asm//:asm", - "@jazzer_ow2_asm//:asm_commons", - "@jazzer_ow2_asm//:asm_tree", + "@org_ow2_asm_asm//jar", + "@org_ow2_asm_asm_commons//jar", + "@org_ow2_asm_asm_tree//jar", ], - visibility = ["//visibility:public"], ) diff --git a/third_party/jacoco_internal.jarjar b/third_party/jacoco_internal.jarjar new file mode 100644 index 00000000..04aa333c --- /dev/null +++ b/third_party/jacoco_internal.jarjar @@ -0,0 +1 @@ +rule org.jacoco.** com.code_intelligence.jazzer.third_party.@0 diff --git a/third_party/libFuzzer.BUILD b/third_party/libFuzzer.BUILD index e8559936..bf902f21 100644 --- a/third_party/libFuzzer.BUILD +++ b/third_party/libFuzzer.BUILD @@ -1,24 +1,35 @@ -# Based on https://github.com/llvm/llvm-project/blob/llvmorg-11.1.0/compiler-rt/lib/fuzzer/build.sh cc_library( - name = "libFuzzer", + name = "libfuzzer_no_main", srcs = glob([ "*.cpp", - ]), + ], exclude = ["FuzzerMain.cpp"]), hdrs = glob([ "*.h", "*.def", ]), - copts = select({ + copts = [ + # https://github.com/llvm/llvm-project/blob/eab395fa4074a5a0cbfebe811937dbb1816df9ef/compiler-rt/CMakeLists.txt#L294-L309 + "-fno-builtin", + "-fno-exceptions", + "-funwind-tables", + "-fno-stack-protector", + "-fno-sanitize=safe-stack", + "-fvisibility=hidden", + "-fno-lto", + ] + select({ "@platforms//os:windows": [ - "/Ox", # Optimize for speed. - "/Oy-", # Do not omit frame pointer. + # https://github.com/llvm/llvm-project/blob/eab395fa4074a5a0cbfebe811937dbb1816df9ef/compiler-rt/CMakeLists.txt#L362-L363 + "/Oy-", + "/GS-", "/std:c++17", ], "//conditions:default": [ - "-g", - "-O2", + # https://github.com/llvm/llvm-project/commit/29d3ba7576b30a37bd19a5d40f304fc39c6ab13d "-fno-omit-frame-pointer", - "-std=c++11", + # https://github.com/llvm/llvm-project/blob/eab395fa4074a5a0cbfebe811937dbb1816df9ef/compiler-rt/CMakeLists.txt#L392 + "-O3", + # Use the same C++ standard as Jazzer itself. + "-std=c++17", ], }), alwayslink = True, diff --git a/third_party/libjpeg_turbo.BUILD b/third_party/libjpeg_turbo.BUILD index 4621f862..e140bc07 100644 --- a/third_party/libjpeg_turbo.BUILD +++ b/third_party/libjpeg_turbo.BUILD @@ -23,13 +23,16 @@ cc_import( cmake( name = "libjpeg_turbo", cache_entries = { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_C_COMPILER": "clang", - "CMAKE_C_FLAGS": "-fsanitize=address,fuzzer-no-link", - "CMAKE_SHARED_LINKER_FLAGS": "-fsanitize=address,fuzzer-no-link", "WITH_JAVA": "1", }, + copts = [ + "-fsanitize=address,fuzzer-no-link", + "-fPIC", + ], lib_source = ":all_files", + linkopts = [ + "-fsanitize=address,fuzzer-no-link", + ], out_shared_libs = [ "libjpeg.so", "libturbojpeg.so", |