Snap for 10453563 from ba37c2e361c2ba91bacc47fcae5383c52e50f6be to mainline-permission-release

Change-Id: I74e7a34d5bfaf65a56951756023cd21521dca326
diff --git a/.bazelrc b/.bazelrc
index 6c60e6c..78b0d87 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,13 +4,7 @@
 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 @@
 
 # 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 af8c8ec..bddfde6 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 0000000..ee17ccc
--- /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 0000000..7953549
--- /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 c32aaff..f410ef4 100644
--- a/.github/workflows/check-formatting.yml
+++ b/.github/workflows/check-formatting.yml
@@ -28,8 +28,8 @@
           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 0000000..2c6bbf5
--- /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 b2d5566..2cbfbaa 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,20 +4,20 @@
   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 @@
         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 @@
       - 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 @@
       - 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 @@
         with:
           name: replayer
           path: replayer.jar
-
diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml
index 2af6f5e..35334b8 100644
--- a/.github/workflows/run-all-tests.yml
+++ b/.github/workflows/run-all-tests.yml
@@ -15,13 +15,7 @@
     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 @@
           - 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 @@
           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()
diff --git a/.gitignore b/.gitignore
index e992903..b0ac4af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 /bazel-*
 .ijwb
 .clwb
+/coverage
diff --git a/Android.bp b/Android.bp
index 8f0b5ba..1f5b45b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -45,10 +45,20 @@
     ],
 }
 
-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 a5ba2f5..5afcd03 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -24,7 +24,7 @@
 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 516b32c..0898a24 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 0000000..b0624cc
--- /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
diff --git a/METADATA b/METADATA
index 96a2a26..ae15e36 100644
--- a/METADATA
+++ b/METADATA
@@ -12,7 +12,7 @@
     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
 }
diff --git a/OWNERS b/OWNERS
index 530afa0..2da7a0b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -2,4 +2,4 @@
 hamzeh@google.com
 kalder@google.com
 cobark@google.com
-anisassi@google.com
+mteffeteller@google.com
diff --git a/README.md b/README.md
index 07bbdda..12137ae 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,10 @@
 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 @@
 
 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,29 +35,35 @@
 
 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)
+* [Clang](https://clang.llvm.org/) and [LLD](https://lld.llvm.org/) 9.0 or later (using a recent version is strongly recommended)
 
-#### Linux
+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.
 
-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:
+#### Compilation
+
+Assuming the dependencies are installed, build Jazzer from source as follows:
 
 ```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.
-./bazelisk-linux-amd64 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
-$ ./bazelisk-linux-amd64 build //:jazzer_release
+$ bazel build //:jazzer_release
 ...
 INFO: Found 1 target...
 Target //:jazzer_release up-to-date:
@@ -69,45 +73,27 @@
 
 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
-# Note the double dash used to pass <arguments> to Jazzer rather than Bazel.
-/path/to/bazelisk-darwin 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
-...
-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
 
 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 @@
 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 @@
 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 @@
    --keep_going=1
 ```
 
-#### 
+####
 
 ### Reproducing a bug
 
@@ -354,6 +341,9 @@
 
 | 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 @@
 | [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 @@
 
 ### 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 @@
 --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 @@
 * `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 @@
 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 @@
 
 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 @@
 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 @@
 
 ```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 c07582b..5589d57 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 @@
     ],
     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 @@
     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 @@
     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 @@
 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 ddafc24..aedbe42 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 @@
     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 b0e4d8c..9107372 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 0000000..cf6acfb
--- /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 0000000..fe68f90
--- /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 0000000..f388c4c
--- /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 0000000..c57babb
--- /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 0000000..e5e66ab
--- /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 0000000..4909018
--- /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 0000000..e2eeadd
--- /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 0000000..57eb880
--- /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 0000000..030d9a9
--- /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 0000000..3694b95
--- /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 0000000..cf73928
--- /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 0000000..60fb8c8
--- /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/jmh/java/com/code_intelligence/jazzer/jmh.bzl b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl
new file mode 100644
index 0000000..5391a46
--- /dev/null
+++ b/agent/src/jmh/java/com/code_intelligence/jazzer/jmh.bzl
@@ -0,0 +1,24 @@
+# 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.
+
+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 0000000..96fd8e1
--- /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 0000000..6e8343c
--- /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 0000000..b55a993
--- /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 0000000..1c09e9a
--- /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 0000000..25fad3b
--- /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 0000000..ce3d629
--- /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 0000000..21f416c
--- /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 0000000..33a0303
--- /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 0000000..2562db1
--- /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 33d0226..f9b026f 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 @@
     } 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 @@
             }
         }
     }.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 @@
             }
         }
     }
+    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)
+        }
+        .filter {
+            instrumentation.isModifiableClass(it)
+        }
+        .toTypedArray()
+
+    instrumentation.addTransformer(runtimeInstrumentor, true)
+
+    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()}")
         }
     }
-    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()}")
-    }
-
-    runtimeInstrumentor.registerCustomHooks(customHooks)
 }
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 2d5eec5..db6ae26 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 @@
     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 fd2a1e7..5d1d28e 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,79 +25,73 @@
 /**
  * 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
+    @Synchronized
+    override fun withIdForClass(className: String, block: (Int) -> Int) {
+        nextEdgeId += block(nextEdgeId)
     }
 }
 
 /**
- * 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
-        }
-    }
-    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()))
-}
-
-/**
  * A strategy for coverage ID generation that synchronizes the IDs assigned to a class with other processes via the
  * specified [idSyncFile].
  * This class takes care of synchronizing the access to the file between multiple processes as long as the general
  * contract of [CoverageIdStrategy] is followed.
- *
- * 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
 
-    var cachedFirstId: Int? = null
-    var cachedClassName: String? = null
-    var cachedIdCount: Int? = null
+    private var cachedFirstId: Int? = null
+    private var cachedClassName: String? = null
+    private var cachedIdCount: Int? = null
+
+    /**
+     * This method is synchronized to prevent concurrent access to the internal file lock which would result in
+     * [java.nio.channels.OverlappingFileLockException]. Furthermore, every coverage ID obtained by [obtainFirstId]
+     * is always committed back again to the sync file by [commitIdCount].
+     */
+    @Synchronized
+    override fun withIdForClass(className: String, block: (Int) -> Int) {
+        var actualNumEdgeIds = 0
+        try {
+            val firstId = obtainFirstId(className)
+            actualNumEdgeIds = block(firstId)
+        } finally {
+            commitIdCount(actualNumEdgeIds)
+        }
+    }
 
     /**
      * Obtains a coverage ID for [className] such that all cooperating agent processes will obtain the same ID.
@@ -108,7 +103,7 @@
      *   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 @@
         }
     }
 
-    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 e2283aa..fe2efd5 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.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.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 @@
         }.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 @@
         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 @@
                 // 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 e573e75..b26bb84 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 @@
         "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 4402a7f..fbde853 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 @@
 /**
  * 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 4d323e5..05837b0 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 @@
 /**
  * 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 f0de4ce..be7c8c8 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 @@
 /**
  * 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 1c564a7..8ed4337 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 @@
 /**
  * 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 e45f760..97adf57 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.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;
+  /**
+   * 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 MethodHandle traceStrcmp = null;
-  private static MethodHandle traceStrstr = null;
-  private static MethodHandle traceMemcmp = null;
+  private static final Class<?> JAZZER_INTERNAL;
 
-  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;
+  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 @@
           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 @@
       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 @@
   /**
    * 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 @@
   @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 @@
   /**
    * 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 @@
   @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 @@
   /**
    * 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 @@
   @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 @@
   /**
    * 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 @@
   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 @@
   /**
    * 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 @@
   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 @@
   /**
    * 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 @@
    */
   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 @@
   /**
    * 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 @@
    */
   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 @@
   /**
    * 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 @@
    */
   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 @@
   /**
    * 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 @@
   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 @@
   /**
    * 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 @@
   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 @@
   /**
    * 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 @@
   @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 @@
   /**
    * 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 @@
    * @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 @@
   /**
    * 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 @@
    * @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 @@
   /**
    * 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,8 +521,62 @@
    * @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();
+    }
+  }
+
+  /**
+   * 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();
     }
@@ -467,19 +584,18 @@
 
   /**
    * 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 @@
     }
   }
 
+  /**
+   * 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 0d17a4a..3a1c5f3 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.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 @@
  * <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 @@
  * 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 @@
    * 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 @@
    * @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 8c34462..3b0d046 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.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.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 @@
   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 @@
       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 @@
                       || 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 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 @@
         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 @@
     }
   }
 
-  // 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 @@
       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 9698053..3d48017 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.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.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 WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache = new WeakHashMap<>();
+  static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache =
+      new WeakHashMap<>();
+  static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache =
+      new WeakHashMap<>();
 
   public static Object autofuzz(FuzzedDataProvider data, Method method) {
     return autofuzz(data, method, null);
@@ -64,22 +71,29 @@
         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 @@
     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 @@
         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 @@
                                     ", ", "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 @@
       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 @@
     }
     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 fceda64..0000000
--- 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 1b52a22..0000000
--- 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/generated/update_java_no_throw_methods_list.sh b/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh
deleted file mode 100755
index 1463c60..0000000
--- a/agent/src/main/java/com/code_intelligence/jazzer/generated/update_java_no_throw_methods_list.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env sh
-# 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.
-
-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
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 50d1070..db93dca 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 @@
         "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 f6728a1..4c3eabc 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 @@
         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 @@
     }
 
     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 6595618..098cf38 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 @@
     }
 
     /**
-     * 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 @@
         }
     }
 
-    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)
+        }
+    }
 
-    fun dumpJacocoCoverage(coveredIds: Set<Int>): ByteArray? {
+    /**
+     * [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
+
         // 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 @@
                 .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 @@
             .asSequence()
             .map { it.replace('/', '.') }
             .toSet()
-        val emptyExecutionDataStore = ExecutionDataStore()
         ClassGraph()
             .enableClassInfo()
             .ignoreClassVisibility()
@@ -209,13 +223,16 @@
                 "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 ba5b7ee..8fb3dc2 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 @@
     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 @@
         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 @@
             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 @@
 
     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 92106e1..ff68ad9 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 @@
 
 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 @@
             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 @@
                 }
             }
 
-            // 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 ac5f178..6db7660 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 7c23c70..1694be5 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 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 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 @@
             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 @@
         // 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 {
+        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
-            }
-        } else {
-            // Push a MethodHandle representing the hooked method.
-            val handleOpcode = when (opcode) {
-                Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL
-                Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE
-                Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC
-                Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL
-                else -> -1
-            }
-            if (java6Mode) {
-                // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50).
-                mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
-            } else {
-                mv.visitLdcInsn(
-                    Handle(
-                        handleOpcode,
-                        owner,
-                        methodName,
-                        methodDescriptor,
-                        isInterface
-                    )
-                ) // push MethodHandle
-            }
-            // Stack layout: ... | MethodHandle (objectref)
-            // Push the owner object again
-            mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
-        }
-        // Stack layout: ... | MethodHandle (objectref) | owner (objectref)
-        // Push a reference to our object array with the saved arguments
-        mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
-        // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref)
-        // Push the hook id
-        mv.visitLdcInsn(hookId)
-        // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
-        // How we proceed depends on the type of hook we want to implement
-        when (hook.hookType) {
-            HookType.BEFORE -> {
-                // Call the hook method
-                mv.visitMethodInsn(
-                    Opcodes.INVOKESTATIC,
-                    hook.hookInternalClassName,
-                    hook.hookMethodName,
-                    hook.hookMethodDescriptor,
-                    false
-                )
-                // Stack layout: ...
-                // Push the values for the original method call onto the stack again
-                if (opcode != Opcodes.INVOKESTATIC) {
-                    mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
-                }
-                loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
-                // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
-                // Call the original method or the next hook in order.
-                visitNextHookTypeOrCall(hookType, true, opcode, owner, methodName, methodDescriptor, isInterface)
-            }
-            HookType.REPLACE -> {
-                // Call the hook method
-                mv.visitMethodInsn(
-                    Opcodes.INVOKESTATIC,
-                    hook.hookInternalClassName,
-                    hook.hookMethodName,
-                    hook.hookMethodDescriptor,
-                    false
-                )
-                // Stack layout: ... | [return value (primitive/objectref)]
-                // Check if we need to process the return value
-                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))
-                        }
-                        // Check if we need to unwrap the returned object
-                        unwrapTypeIfPrimitive(returnTypeDescriptor)
-                    }
-                }
-            }
-            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
+                // 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
                 }
-                // 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
+            } else {
+                // Push a MethodHandle representing the hooked method.
+                val handleOpcode = when (opcode) {
+                    Opcodes.INVOKEVIRTUAL -> Opcodes.H_INVOKEVIRTUAL
+                    Opcodes.INVOKEINTERFACE -> Opcodes.H_INVOKEINTERFACE
+                    Opcodes.INVOKESTATIC -> Opcodes.H_INVOKESTATIC
+                    Opcodes.INVOKESPECIAL -> Opcodes.H_INVOKESPECIAL
+                    else -> -1
+                }
+                if (java6Mode) {
+                    // MethodHandle constants (type 15) are not supported in Java 6 class files (major version 50).
+                    mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+                } else {
+                    mv.visitLdcInsn(
+                        Handle(
+                            handleOpcode,
+                            owner,
+                            methodName,
+                            methodDescriptor,
+                            isInterface
+                        )
+                    ) // push MethodHandle
+                }
+                // Stack layout: ... | MethodHandle (objectref)
+                // Push the owner object again
+                mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj)
+            }
+            // Stack layout: ... | MethodHandle (objectref) | owner (objectref)
+            // Push a reference to our object array with the saved arguments
+            mv.visitVarInsn(Opcodes.ALOAD, localObjArr)
+            // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref)
+            // Push the hook id
+            mv.visitLdcInsn(hookId)
+            // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+            // How we proceed depends on the type of hook we want to implement
+            when (hook.hookType) {
+                HookType.BEFORE -> {
+                    // Call the hook method
+                    mv.visitMethodInsn(
+                        Opcodes.INVOKESTATIC,
+                        hook.hookInternalClassName,
+                        hook.hookMethodName,
+                        hook.hookMethodDescriptor,
+                        false
+                    )
+
+                    // Call the original method if this is the last BEFORE hook. If not, the original method will be
+                    // called by the next AFTER hook.
+                    if (index == matchingHooks.lastIndex) {
+                        // Stack layout: ...
+                        // Push the values for the original method call onto the stack again
+                        if (opcode != Opcodes.INVOKESTATIC) {
+                            mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+                        }
+                        loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+                        // Stack layout: ... | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+                        mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+                    }
+                }
+                HookType.REPLACE -> {
+                    // Call the hook method
+                    mv.visitMethodInsn(
+                        Opcodes.INVOKESTATIC,
+                        hook.hookInternalClassName,
+                        hook.hookMethodName,
+                        hook.hookMethodDescriptor,
+                        false
+                    )
+                    // Stack layout: ... | [return value (primitive/objectref)]
+                    // Check if we need to process the return value
+                    if (returnTypeDescriptor != "V") {
+                        val hookMethodReturnType = extractReturnTypeDescriptor(hook.hookMethodDescriptor)
+                        // if the hook method's return type is primitive we don't need to unwrap or cast it
+                        if (!isPrimitiveType(hookMethodReturnType)) {
+                            // Check if the returned object type is different than the one that should be returned
+                            // If a primitive should be returned we check it's wrapper type
+                            val expectedType = getWrapperTypeDescriptor(returnTypeDescriptor)
+                            if (expectedType != hookMethodReturnType) {
+                                // Cast object
+                                mv.visitTypeInsn(Opcodes.CHECKCAST, extractInternalClassName(expectedType))
+                            }
+                            // Check if we need to unwrap the returned object
+                            unwrapTypeIfPrimitive(returnTypeDescriptor)
+                        }
+                    }
+                }
+                HookType.AFTER -> {
+                    // Call the original method before the first AFTER hook
+                    if (index == 0 || matchingHooks[index - 1].hookType != HookType.AFTER) {
+                        // Push the values for the original method call again onto the stack
+                        if (opcode != Opcodes.INVOKESTATIC) {
+                            mv.visitVarInsn(Opcodes.ALOAD, localOwnerObj) // push owner object
+                        }
+                        loadMethodArguments(paramDescriptors, localObjArr) // push all method arguments
+                        // Stack layout: ... | MethodHandle (objectref) | owner (objectref) | object array (arrayref) | hookId (int)
+                        //                   | [owner (objectref)] | arg1 (primitive/objectref) | arg2 (primitive/objectref) | ...
+                        mv.visitMethodInsn(opcode, owner, methodName, methodDescriptor, isInterface)
+                        if (returnTypeDescriptor == "V") {
+                            // If the method didn't return anything, we push a nullref as placeholder
+                            mv.visitInsn(Opcodes.ACONST_NULL) // push nullref
+                        }
+                        // Wrap return value if it is a primitive type
+                        wrapTypeIfPrimitive(returnTypeDescriptor)
+                        mv.visitVarInsn(Opcodes.ASTORE, localReturnObj) // consume objectref
+                    }
                     mv.visitVarInsn(Opcodes.ALOAD, localReturnObj) // push objectref
-                    // 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 @@
         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 @@
     }
 
     // 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 0000000..66a21ee
--- /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 86ad45a..7879384 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 0000000..0512ec2
--- /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 e6d3176..65f11e5 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 @@
     }
 
     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 c2092b3..0000000
--- 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 df28adb..08bd765 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 @@
 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 fc6bfc4..0a250d1 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 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 @@
     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 @@
       }
     }
   }
-
-  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 095b0bf..0d8162d 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(
-    name = "signal_handler",
-    srcs = ["SignalHandler.java"],
-    javacopts = [
-        "-XDenableSunApiLintControl",
+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",
     ],
 )
 
-kt_jvm_library(
+java_jni_library(
+    name = "signal_handler",
+    srcs = ["SignalHandler.java"],
+    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__",
+    ],
+)
+
+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 af2424a..4069d25 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);
-
-  public static void enlargeCoverageMap() {
-    registerNewCoverageCounters();
-    System.out.println("INFO: New number of inline 8-bit counters: " + mem.capacity());
+  static {
+    RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
   }
 
-  private static native void registerNewCoverageCounters();
+  private static final String ENV_MAX_NUM_COUNTERS = "JAZZER_MAX_NUM_COUNTERS";
+
+  private static final int MAX_NUM_COUNTERS = System.getenv(ENV_MAX_NUM_COUNTERS) != null
+      ? Integer.parseInt(System.getenv(ENV_MAX_NUM_COUNTERS))
+      : 1 << 20;
+
+  private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+
+  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);
+    }
+  }
+
+  /**
+   * 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 fe4d8ac..b7aad33 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;
+import com.github.fmeum.rules_jni.RulesJni;
+import sun.misc.Unsafe;
 
-public class FuzzedDataProviderImpl implements FuzzedDataProvider {
-  public FuzzedDataProviderImpl() {}
+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 @@
 
   @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 @@
 
   @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 @@
 
   @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 @@
   @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 8bc1b38..79c851a 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 @@
     // 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 976e024..4eb8022 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 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() {}
-
-  // Called from native code.
-  public static FuzzedDataProvider makeFuzzedDataProviderProxy() {
-    return (FuzzedDataProvider) Proxy.newProxyInstance(
-        RecordingFuzzedDataProvider.class.getClassLoader(), new Class[] {FuzzedDataProvider.class},
-        new RecordingFuzzedDataProvider());
+  private RecordingFuzzedDataProvider(FuzzedDataProvider target) {
+    this.target = target;
   }
 
-  // Called from native code.
+  public static FuzzedDataProvider makeFuzzedDataProviderProxy(FuzzedDataProvider target) {
+    return new RecordingFuzzedDataProvider(target);
+  }
+
   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 @@
     }
     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 0a42aa9..49ee80c 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 352da8e..37e8eae 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.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 @@
     }
   }
 
+  @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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
     // 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 456d0cb..821ade0 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);
+  static {
+    RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+  }
 
-  // Calls: void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
-  public static native void traceConstCmpInt(int arg1, int arg2, 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_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
-  public static native void traceCmpLong(long arg1, long arg2, int pc);
-
-  // Calls: void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);
-  public static native void traceSwitch(long val, long[] cases, int pc);
-
-  // 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);
+  public static void traceStrcmp(String s1, String s2, int result, int pc) {
+    traceMemcmp(encodeForLibFuzzer(s1), encodeForLibFuzzer(s2), result, 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);
-
-  /* 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 @@
 
   // 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 0000000..81f2a20
--- /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 5e301ef..10e3477 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 @@
     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 1f09afe..44249c8 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.**",
+    "jdk.**",
+    "sun.**",
+    "com.sun.**", // package for Proxy objects
+    // Azul JDK internals
+    "com.azul.tooling.**",
+    // Kotlin internals
+    "kotlin.**",
+    // Jazzer internals
+    "com.code_intelligence.jazzer.**",
     "jaz.Ter", // safe companion of the honeypot class used by sanitizers
     "jaz.Zer", // honeypot class used by sanitizers
-    "jdk.**",
-    "kotlin.**",
-    "sun.**",
-)
+) + 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
similarity index 95%
rename from agent/src/main/java/com/code_intelligence/jazzer/runtime/ExceptionUtils.kt
rename to agent/src/main/java/com/code_intelligence/jazzer/utils/ExceptionUtils.kt
index 31a6174..30f6fb3 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 @@
             }
         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
similarity index 93%
rename from agent/src/main/java/com/code_intelligence/jazzer/runtime/ManifestUtils.kt
rename to agent/src/main/java/com/code_intelligence/jazzer/utils/ManifestUtils.kt
index d88c3e1..e7165e5 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 af8cce9..1b399ba 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 @@
 
 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 @@
     }
     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 0000000..c6cdcf1
--- /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
similarity index 100%
rename from sanitizers/src/main/java/jaz/Ter.java
rename to 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 0000000..08ca3d2
--- /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 6b75fb8..0000000
--- 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 c4bdfcf..0000000
--- 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 0000000..7d91047
--- /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 0000000..2600a53
--- /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 66a85db..59ef238 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 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 9192ff7..f2537b7 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 @@
     ],
     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 0615e9a..0906d1d 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.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 @@
   }
 
   @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 @@
     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 @@
         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 52f19a7..d556beb 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.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 @@
   }
 
   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 53efd20..c5a2e15 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 472d2b9..036559e 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 @@
         "DynamicTestContract.java",
         "PatchTestUtils.kt",
     ],
+    visibility = ["//visibility:public"],
 )
 
 wrapped_kt_jvm_test(
@@ -130,6 +131,7 @@
     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 31e9733..4fde7ee 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 15c88f4..f2cf2f0 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 @@
     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 @@
         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 @@
         // 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 7e7c31c..ac263dc 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 @@
 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 @@
         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 2723ad6..0df349c 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.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 @@
     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 @@
   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 787ea49..3ea33d1 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 @@
 
 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 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 f286d03..00279c3 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
+
+object PatchTestUtils {
+    @JvmStatic
+    fun classToBytecode(targetClass: Class<*>): ByteArray {
+        return ClassLoader
+            .getSystemClassLoader()
+            .getResourceAsStream("${targetClass.name.replace('.', '/')}.class")!!
+            .use {
+                it.readBytes()
+            }
+    }
+
+    @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) }
+    }
+
+    /**
+     * 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)
         }
-}
-
-fun bytecodeToClass(name: String, bytecode: ByteArray): Class<*> {
-    return BytecodeClassLoader(name, bytecode).loadClass(name)
-}
-
-/**
- * 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 a71e118..7e31b77 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.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 @@
   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
similarity index 65%
rename from driver/testdata/test/SimpleFuzzTarget.java
rename to agent/src/test/java/com/code_intelligence/jazzer/instrumentor/ReplaceHooksInit.java
index 5657e41..da77be8 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 76fb53e..b6266d1 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 7a4b89f..fadbdf8 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 @@
       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 48f16e6..d8e2888 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 @@
 
   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 c6fd218..4d4b031 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 06bed14..a919242 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 @@
   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 0000000..97ac4f6
--- /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 0000000..5e922fc
--- /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 0000000..d58a5ca
--- /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 0000000..9275ca3
--- /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 0000000..5742476
--- /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 1e2348c..e69de29 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 59b1584..0000000
--- 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 65d298d..0000000
--- 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 0000000..b3dc986
--- /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 0000000..626fdc7
--- /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 bd90e50..c70543b 100644
--- a/bazel/fuzz_target.bzl
+++ b/bazel/fuzz_target.bzl
@@ -17,7 +17,7 @@
         target_class = None,
         deps = [],
         hook_classes = [],
-        native_libs = [],
+        data = [],
         sanitizer = None,
         visibility = None,
         tags = [],
@@ -25,6 +25,12 @@
         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 @@
         **kwargs
     )
 
-    additional_args = []
-
     if sanitizer == None:
         driver = "//driver:jazzer_driver"
     elif sanitizer == "address":
@@ -60,19 +64,39 @@
 
     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 0000000..b4de362
--- /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 0000000..becfe75
--- /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 0000000..107d852
--- /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 0000000..2a567c6
--- /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
deleted file mode 100755
index 22e49af..0000000
--- a/bazelisk-linux-amd64
+++ /dev/null
Binary files differ
diff --git a/deploy/BUILD.bazel b/deploy/BUILD.bazel
index 29f9a5a..7cceeae 100644
--- a/deploy/BUILD.bazel
+++ b/deploy/BUILD.bazel
@@ -2,8 +2,8 @@
 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 78c57f7..6c17f12 100755
--- a/docker/jazzer-autofuzz/entrypoint.sh
+++ b/docker/jazzer-autofuzz/entrypoint.sh
@@ -17,8 +17,6 @@
 
 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 56787be..bddfcb5 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 becd4fe..2d503cc 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"],
+    name = "jazzer_main",
+    srcs = ["jazzer_main.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__",
-    ],
-    deps = [
-        "@com_google_absl//absl/strings:str_format",
-        "@fmeum_rules_jni//jni",
-    ],
-)
-
-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": [],
-    }),
-    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",
+        ":jvm_tooling_lib",
         "@com_google_absl//absl/strings",
-        "@com_google_absl//absl/strings:str_format",
-        "@com_google_glog//:glog",
         "@fmeum_rules_jni//jni:libjvm",
         "@jazzer_com_github_gflags_gflags//:gflags",
     ],
 )
 
-cc_17_library(
-    name = "driver_lib",
-    srcs = [
-        "libfuzzer_fuzz_target.cpp",
+cc_library(
+    name = "jvm_tooling_lib",
+    srcs = ["jvm_tooling.cpp"],
+    hdrs = ["jvm_tooling.h"],
+    tags = [
+        # Should be built through the cc_17_library driver_lib.
+        "manual",
     ],
-    linkstatic = True,
     deps = [
-        ":jvm_tooling_lib",
-        "@jazzer_libfuzzer//:libFuzzer",
+        "@bazel_tools//tools/cpp/runfiles",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
+        "@fmeum_rules_jni//jni",
+        "@jazzer_com_github_gflags_gflags//:gflags",
     ],
+)
+
+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 @@
 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 @@
         "//conditions:default": [
             "-fsanitize=address",
             "-static-libsan",
-            "-rdynamic",
         ],
     }) + select({
         "//:clang_on_linux": ["-fuse-ld=lld"],
@@ -161,21 +132,23 @@
     }),
     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 @@
             # Link UBSan statically, even on macOS.
             "-static-libsan",
             "-fsanitize-link-c++-runtime",
-            "-rdynamic",
         ],
     }) + select({
         "//:clang_on_linux": ["-fuse-ld=lld"],
@@ -195,32 +167,26 @@
     }),
     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(
     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 @@
     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 0a57608..0000000
--- 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/coverage_tracker.h b/driver/coverage_tracker.h
deleted file mode 100644
index 5b237de..0000000
--- a/driver/coverage_tracker.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 <jni.h>
-
-#include <string>
-
-#include "jvm_tooling.h"
-
-namespace jazzer {
-
-// The members of this struct are only accessed by libFuzzer.
-struct __attribute__((packed)) PCTableEntry {
-  [[maybe_unused]] uintptr_t PC, PCFlags;
-};
-
-// 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 {
- 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);
-};
-}  // namespace jazzer
diff --git a/driver/fuzz_target_runner.cpp b/driver/fuzz_target_runner.cpp
deleted file mode 100644
index 934e27e..0000000
--- 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 98ac794..0000000
--- 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 9b8faf7..0000000
--- 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 210bf11..e6225b7 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 @@
   // 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 @@
 
 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 ed4c675..0000000
--- 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 b3202b1..0000000
--- 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 7d58e1f..0000000
--- 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 0000000..c72e111
--- /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 178eec0..71a5f58 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 @@
 // 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 @@
             "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 @@
   // 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 @@
     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 @@
   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 @@
 
   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 @@
   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 @@
   // 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 @@
         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 @@
 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 be9582d..2a4a133 100644
--- a/driver/jvm_tooling.h
+++ b/driver/jvm_tooling.h
@@ -42,46 +42,5 @@
 
   // 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 f2e8c66..5aceadd 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 @@
 
 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 @@
 
 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 @@
     } 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 5b7813d..0000000
--- 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 57beef5..0000000
--- 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 557277a..0000000
--- 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 d258e51..0000000
--- 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 0000000..4b58188
--- /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 bb3ec5e..0000000
--- 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 71d1527..0000000
--- 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 7d84fea..0000000
--- 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 05e5953..0000000
--- 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/signal_handler.h b/driver/signal_handler.h
deleted file mode 100644
index d0d1712..0000000
--- a/driver/signal_handler.h
+++ /dev/null
@@ -1,29 +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>
-
-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
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 0000000..c8e6ba1
--- /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 0000000..5b107ad
--- /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 0000000..5646e91
--- /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 0000000..477c7d3
--- /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 0000000..d9cb1e9
--- /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 0000000..0c7721c
--- /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 0000000..37eb1d0
--- /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 0000000..863a187
--- /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 0000000..dc8349d
--- /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/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h
new file mode 100644
index 0000000..8ccecee
--- /dev/null
+++ b/driver/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h
@@ -0,0 +1,42 @@
+/*
+ * 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>
+
+namespace jazzer {
+
+// The members of this struct are only accessed by libFuzzer.
+struct __attribute__((packed)) PCTableEntry {
+  [[maybe_unused]] uintptr_t PC, PCFlags;
+};
+
+// CoverageTracker registers an array of 8-bit coverage counters with
+// libFuzzer. The array is populated from Java using Unsafe.
+class CoverageTracker {
+ private:
+  static uint8_t *counters_;
+  static PCTableEntry *pc_entries_;
+
+ public:
+  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 0000000..6231af0
--- /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
similarity index 82%
rename from driver/libfuzzer_callbacks.h
rename to driver/src/main/native/com/code_intelligence/jazzer/driver/fuzz_target_runner.h
index 985809a..0e8846c 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
similarity index 85%
rename from driver/fuzzed_data_provider.cpp
rename to driver/src/main/native/com/code_intelligence/jazzer/driver/fuzzed_data_provider.cpp
index e8cb971..494bb9e 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 @@
   }
   // 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 @@
 
 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 @@
 
 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 @@
 
 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 @@
 // 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 @@
 
 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 @@
 }  // 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 @@
     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 0000000..8764aaa
--- /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 0000000..a20863f
--- /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
similarity index 96%
rename from driver/sanitizer_hooks_with_pc.h
rename to driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_hooks_with_pc.h
index d986131..be655ad 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_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
similarity index 79%
rename from driver/sanitizer_symbols.cpp
rename to driver/src/main/native/com/code_intelligence/jazzer/driver/sanitizer_symbols.cpp
index 10255ef..abc5f04 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 0000000..8e6d19a
--- /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 0000000..0411970
--- /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 0000000..d8f048e
--- /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 0000000..87cda2b
--- /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 bf33517..14340b8 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 8dd67e1..c3c2443 100644
--- a/driver/testdata/BUILD.bazel
+++ b/driver/testdata/BUILD.bazel
@@ -3,8 +3,4 @@
     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/FuzzTargetWithCoverage.java b/driver/testdata/test/FuzzTargetWithCoverage.java
deleted file mode 100644
index 599b1fa..0000000
--- a/driver/testdata/test/FuzzTargetWithCoverage.java
+++ /dev/null
@@ -1,28 +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.runtime.CoverageMap;
-
-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);
-  }
-}
diff --git a/driver/testdata/test/FuzzTargetWithDataProvider.java b/driver/testdata/test/FuzzTargetWithDataProvider.java
deleted file mode 100644
index fc5bc1b..0000000
--- 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/testdata/test/FuzzTargetWithInit.java b/driver/testdata/test/FuzzTargetWithInit.java
deleted file mode 100644
index 86aed82..0000000
--- a/driver/testdata/test/FuzzTargetWithInit.java
+++ /dev/null
@@ -1,30 +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;
-
-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");
-      }
-    }
-  }
-}
diff --git a/driver/utils.cpp b/driver/utils.cpp
deleted file mode 100644
index 4d8042e..0000000
--- 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 dde8aae..599b826 100644
--- a/examples/BUILD.bazel
+++ b/examples/BUILD.bazel
@@ -5,6 +5,7 @@
 
 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 @@
     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 @@
     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 @@
     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 @@
     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(
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     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 @@
 
 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 @@
     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 acc023a..b68ef6f 100644
--- a/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java
+++ b/examples/src/main/java/com/example/ExampleValueProfileFuzzer.java
@@ -32,14 +32,14 @@
     // 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 a6898bf..ba3e7c8 100644
--- a/examples/src/main/java/com/example/JpegImageParserFuzzer.java
+++ b/examples/src/main/java/com/example/JpegImageParserFuzzer.java
@@ -22,20 +22,6 @@
 
 // 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 0000000..9d3448c
--- /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 7f23f75..4c44327 100644
--- a/examples/src/main/native/com/example/BUILD.bazel
+++ b/examples/src/main/native/com/example/BUILD.bazel
@@ -9,6 +9,13 @@
         "-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": [
diff --git a/format.sh b/format.sh
index f783f6e..f1440f1 100755
--- a/format.sh
+++ b/format.sh
@@ -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 @@
 
 # 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 0000000..8fd2c9f
--- /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 "$@"
diff --git a/maven.bzl b/maven.bzl
index 29f3845..a13f175 100644
--- a/maven.bzl
+++ b/maven.bzl
@@ -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 16303c0..cda4c94 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 36b3444..7abffd8 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 @@
     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 @@
         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 @@
             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 fa84208..fdc616a 100644
--- a/sanitizers/BUILD.bazel
+++ b/sanitizers/BUILD.bazel
@@ -3,6 +3,5 @@
     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 8bdea7a..cef4cf4 100644
--- a/sanitizers/sanitizers.bzl
+++ b/sanitizers/sanitizers.bzl
@@ -17,8 +17,13 @@
 _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 6548065..1b156f9 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 f6401df..55691c1 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 @@
 /**
  * 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 9b1e8ca..1dc1d5f 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 @@
 /**
  * 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 @@
             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 @@
         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 0000000..1afd614
--- /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 2d4fb9c..56e12f0 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 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 @@
     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 @@
         ),
         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 0000000..d3adc20
--- /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 7842d87..0fcabe3 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 0000000..def5f6e
--- /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 0000000..1043ac0
--- /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 0000000..f317bcc
--- /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 3166773..219490d 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 @@
  * 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 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 0000000..c725844
--- /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 0000000..fd6ac72
--- /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 81275a3..0000000
--- 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 0b27609..0000000
--- 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 d148545..5d2e1ca 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 @@
     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 @@
         "@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 0000000..c3fa47a
--- /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 0000000..911db1d
--- /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 0000000..b3dfee7
--- /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/SimpleFuzzTarget.java b/sanitizers/src/test/java/com/example/LibraryLoad.java
similarity index 60%
copy from driver/testdata/test/SimpleFuzzTarget.java
copy to sanitizers/src/test/java/com/example/LibraryLoad.java
index 5657e41..8141176 100644
--- a/driver/testdata/test/SimpleFuzzTarget.java
+++ b/sanitizers/src/test/java/com/example/LibraryLoad.java
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package test;
+package com.example;
 
-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");
+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 0000000..f5d5278
--- /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 0000000..c620a75
--- /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 7f85e48..e6b62b4 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 @@
     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 0000000..e2d0b72
--- /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 0000000..a548cfb
--- /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 0000000..21986e3
--- /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 0000000..8a16b5c
--- /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/SimpleFuzzTarget.java b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java
similarity index 63%
copy from driver/testdata/test/SimpleFuzzTarget.java
copy to sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java
index 5657e41..b674f5c 100644
--- a/driver/testdata/test/SimpleFuzzTarget.java
+++ b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java
@@ -12,14 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package test;
+package com.example.ldap;
 
-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");
-    }
+import java.util.Hashtable;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.spi.InitialContextFactory;
+
+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 0000000..a51fadc
--- /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 0000000..cbc7743
--- /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 0000000..16240ef
--- /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/libfuzzer_callbacks.h b/tests/src/test/java/com/example/BytesMemoryLeakFuzzer.java
similarity index 77%
copy from driver/libfuzzer_callbacks.h
copy to tests/src/test/java/com/example/BytesMemoryLeakFuzzer.java
index 985809a..9540631 100644
--- a/driver/libfuzzer_callbacks.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,12 +14,8 @@
  * limitations under the License.
  */
 
-#pragma once
+package com.example;
 
-#include <jni.h>
-
-namespace jazzer {
-
-bool registerFuzzerCallbacks(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 0000000..8f63639
--- /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 0000000..430bfa4
--- /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 0000000..9f00512
--- /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 0000000..88627f4
--- /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 0000000..2428d21
--- /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 0000000..324764d
--- /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
new file mode 100644
index 0000000..f18c9a6
--- /dev/null
+++ b/tests/src/test/java/com/example/LongStringFuzzerInput
Binary files differ
diff --git a/driver/utils.h b/tests/src/test/java/com/example/MemoryLeakFuzzer.java
similarity index 62%
rename from driver/utils.h
rename to tests/src/test/java/com/example/MemoryLeakFuzzer.java
index 99d7b60..9f38a1e 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 0000000..1085a95
--- /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/driver/testdata/test/SimpleFuzzTarget.java b/tests/src/test/java/com/example/NoCoverageFuzzer.java
similarity index 60%
copy from driver/testdata/test/SimpleFuzzTarget.java
copy to tests/src/test/java/com/example/NoCoverageFuzzer.java
index 5657e41..a1f8b4e 100644
--- a/driver/testdata/test/SimpleFuzzTarget.java
+++ b/tests/src/test/java/com/example/NoCoverageFuzzer.java
@@ -1,4 +1,4 @@
-// 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.
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package test;
+package com.example;
 
-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 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 0000000..bf1c110
--- /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 0000000..4d1e4e8
--- /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 0000000..93b886a
--- /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 0000000..2edcc26
--- /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 0643e4f..a234e83 100644
--- a/third_party/BUILD.bazel
+++ b/third_party/BUILD.bazel
@@ -12,3 +12,5 @@
     },
     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 2f659fc..0000000
--- 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 3885fa1..03cfe5e 100644
--- a/third_party/jacoco-make-probe-inserter-subclassable.patch
+++ b/third_party/jacoco-make-probe-inserter-subclassable.patch
@@ -63,21 +63,8 @@
 +    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 @@
 -	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 @@
  			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 9e6140a..38ac7f6 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 0000000..04aa333
--- /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 e855993..bf902f2 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 4621f86..e140bc0 100644
--- a/third_party/libjpeg_turbo.BUILD
+++ b/third_party/libjpeg_turbo.BUILD
@@ -23,13 +23,16 @@
 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",