Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ http_archive(
http_archive(
name = "jazzer_libfuzzer",
build_file = "//third_party:libFuzzer.BUILD",
patch_args = ["-p1"],
patches = ["//third_party:libfuzzer-exit-on-time.patch"],
sha256 = "200b32c897b1171824462706f577d7f1d6175da602eccfe570d2dceeac11d490",
strip_prefix = "llvm-project-jazzer-2023-04-25/compiler-rt/lib/fuzzer",
url = "https://github.com/CodeIntelligenceTesting/llvm-project-jazzer/archive/refs/tags/2023-04-25.tar.gz",
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,24 @@ Currently, this applies to Server-Side Request Forgery and File Path Traversal s
For details, check out the [API documentation](https://codeintelligencetesting.github.io/jazzer-docs/jazzer-api/com/code_intelligence/jazzer/api/BugDetectors.html).


## Stopping criteria: exit_on_time and exit_on_time_min_runs

By default Jazzer runs indefinitely. Two options let you define when a successful fuzz run should stop automatically:

- **`--exit_on_time=<seconds>`** - exit successfully after the specified number of seconds pass without any new coverage being discovered. `0` (default) disables the limit. Equivalent to libFuzzer's `-exit_on_time`, but forwarded automatically by the Jazzer driver.

- **`--exit_on_time_min_runs=<N>`** (default: `100000`) - minimum number of fuzzer executions that must complete before `--exit_on_time` is allowed to trigger. Prevents the fuzzer from stopping too early during the warm-up phase when the corpus is being loaded and coverage has not yet stabilised.

Example — stop after 8 hours of no new coverage, but only after at least 1 000 000 runs:

```shell
./jazzer --cp=fuzz.jar \
--target_class=com.example.MyFuzzTest \
--exit_on_time=28800 \
--exit_on_time_min_runs=1000000 \
corpus/
```

## OSS-Fuzz

[Code Intelligence](https://code-intelligence.com) and Google have teamed up to bring support for Java, Kotlin, and other JVM-based languages to [OSS-Fuzz](https://github.com/google/oss-fuzz), Google's project for large-scale fuzzing of open-source software.
Expand Down
11 changes: 11 additions & 0 deletions docs/arguments-and-configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ Some parameters only have an effect when used with standalone Jazzer binary (mar
- **dedup** [bool, default="true"]
- Compute and print a deduplication token for every finding

- **exit_on_time** [uint64, default="0"]
- Exit fuzzing successfully if no new coverage features are discovered for the specified number
of seconds. `0` disables the limit. This option is supported for single-process fuzzing only.
A native `-exit_on_time` flag takes precedence over this option.

- **exit_on_time_min_runs** [uint64, default="100000"]
- Minimum number of fuzzer runs that must complete before `--exit_on_time` is allowed to trigger.
Prevents premature exits during the warm-up phase when coverage has not yet stabilised.
Has no effect if `--exit_on_time` is `0` (disabled).
A native `-exit_on_time_min_runs` flag takes precedence over this option.

- **disabled_hooks** [list, separator=`':'`, default=""]
- Names of classes from which hooks (custom or built-in) should not be loaded from
- Example: to disable the `ServerSideRequestForgery` and `RegexInjection` sanitizers use this environment variable when running Jazzer:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ java_library(
srcs = ["Driver.java"],
visibility = [
"//src/main/java/com/code_intelligence/jazzer:__pkg__",
"//src/test/java/com/code_intelligence/jazzer/driver:__pkg__",
],
deps = [
":fuzz_target_finder",
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/code_intelligence/jazzer/driver/Driver.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ public static int start(List<String> args, boolean spawnsSubprocesses) throws IO
args.add("-runs=" + Opt.maxExecutions.get());
}
}
try {
translateExitOnTimeOptions(args, Opt.exitOnTime.get(), Opt.exitOnTimeMinRuns.get());
} catch (IllegalArgumentException e) {
Log.error(e.getMessage());
exit(1);
}

// Installing the agent after the following "findFuzzTarget" leads to an asan error
// in it on "Class.forName(targetClassName)", but only during native fuzzing.
Expand All @@ -181,6 +187,44 @@ public static int start(List<String> args, boolean spawnsSubprocesses) throws IO
return FuzzTargetRunner.startLibFuzzer(args);
}

/**
* Translates --exit_on_time and --exit_on_time_min_runs into the corresponding libFuzzer flags.
*
* @throws IllegalArgumentException if a value does not fit into libFuzzer's int flags
*/
static void translateExitOnTimeOptions(
List<String> args, long exitOnTime, long exitOnTimeMinRuns) {
// Opt has already consumed these Jazzer-specific options. Never forward them to libFuzzer,
// which only accepts the single-dash spelling and would otherwise emit a warning.
args.removeIf(a -> a.startsWith("--exit_on_time=") || a.startsWith("--exit_on_time_min_runs="));
if (exitOnTime == 0) {
return;
}
checkFitsIntoLibFuzzerIntFlag("exit_on_time", exitOnTime);
checkFitsIntoLibFuzzerIntFlag("exit_on_time_min_runs", exitOnTimeMinRuns);
// A native libFuzzer flag takes precedence over the Jazzer option, just like
// -max_total_time does for --max_duration.
boolean hasExitOnTime = args.stream().anyMatch(a -> a.startsWith("-exit_on_time="));
if (!hasExitOnTime) {
args.add("-exit_on_time=" + exitOnTime);
}
boolean hasExitOnTimeMinRuns =
args.stream().anyMatch(a -> a.startsWith("-exit_on_time_min_runs="));
if (!hasExitOnTimeMinRuns) {
args.add("-exit_on_time_min_runs=" + exitOnTimeMinRuns);
}
}

private static void checkFitsIntoLibFuzzerIntFlag(String name, long value) {
// Opt values are unsigned 64-bit integers, but libFuzzer would silently truncate them to int.
if (Long.compareUnsigned(value, Integer.MAX_VALUE) > 0) {
throw new IllegalArgumentException(
String.format(
"--%s must be at most %d, got %s",
name, Integer.MAX_VALUE, Long.toUnsignedString(value)));
}
}

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
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/code_intelligence/jazzer/driver/Opt.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ public final class Opt {
"Glob patterns matching names of classes to instrument with hooks (custom and built-in)");
public static final OptItem<Boolean> dedup =
boolSetting("dedup", true, "Compute and print a deduplication token for every finding");
public static final OptItem<Long> exitOnTime =
uint64Setting(
"exit_on_time",
0,
"Exit fuzzing successfully if no new coverage features are discovered for the specified"
+ " number of seconds (0 disables the limit, at most 2147483647). Single-process"
+ " fuzzing only.");
public static final OptItem<Long> exitOnTimeMinRuns =
uint64Setting(
"exit_on_time_min_runs",
100000,
"Minimum number of fuzzer runs before --exit_on_time is allowed to trigger"
+ " (default: 100000, at most 2147483647).");
public static final OptItem<List<String>> disabledHooks =
stringListSetting(
"disabled_hooks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ public static FuzzTestExecutor prepare(ExtensionContext context, Optional<Path>
if (Opt.maxExecutions.get() > 0) {
libFuzzerArgs.add("-runs=" + Opt.maxExecutions.get());
}
if (Opt.exitOnTime.get() > 0) {
libFuzzerArgs.add(
"-exit_on_time=" + checkedLibFuzzerIntFlag("exit_on_time", Opt.exitOnTime.get()));
libFuzzerArgs.add(
"-exit_on_time_min_runs="
+ checkedLibFuzzerIntFlag("exit_on_time_min_runs", Opt.exitOnTimeMinRuns.get()));
}
// Disable libFuzzer's out of memory detection: It is only useful for native library fuzzing,
// which we don't support without our native driver, and leads to false positives where it picks
// up IntelliJ's memory usage.
Expand All @@ -137,6 +144,17 @@ public static FuzzTestExecutor prepare(ExtensionContext context, Optional<Path>
return new FuzzTestExecutor(libFuzzerArgs, javaSeedsDir);
}

private static long checkedLibFuzzerIntFlag(String name, long value) {
// Opt values are unsigned 64-bit integers, but libFuzzer would silently truncate them to int.
if (Long.compareUnsigned(value, Integer.MAX_VALUE) > 0) {
throw new FuzzTestConfigurationError(
String.format(
"%s must be at most %d, got %s",
name, Integer.MAX_VALUE, Long.toUnsignedString(value)));
}
return value;
}

private static Optional<String> translateJUnitTimeoutToLibFuzzerFlag(ExtensionContext context) {
return Stream.<Supplier<Optional<Long>>>of(
() ->
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ java_test(
],
)

java_junit5_test(
name = "DriverTest",
srcs = ["DriverTest.java"],
deps = JUNIT5_DEPS + [
"//src/main/java/com/code_intelligence/jazzer/driver",
"@maven//:com_google_truth_truth",
"@maven//:org_junit_jupiter_junit_jupiter_api",
],
)

java_junit5_test(
name = "OptItemTest",
srcs = ["OptItemTest.java"],
Expand Down
93 changes: 93 additions & 0 deletions src/test/java/com/code_intelligence/jazzer/driver/DriverTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2026 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.code_intelligence.jazzer.driver;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;

public class DriverTest {
private static List<String> translate(long exitOnTime, long exitOnTimeMinRuns, String... args) {
List<String> result = new ArrayList<>(Arrays.asList(args));
Driver.translateExitOnTimeOptions(result, exitOnTime, exitOnTimeMinRuns);
return result;
}

@Test
void exitOnTimeWithMinRuns() {
assertThat(translate(2, 1, "--exit_on_time=2", "--exit_on_time_min_runs=1"))
.containsExactly("-exit_on_time=2", "-exit_on_time_min_runs=1")
.inOrder();
}

@Test
void exitOnTimeWithDefaultMinRuns() {
assertThat(translate(2, 100000, "--exit_on_time=2"))
.containsExactly("-exit_on_time=2", "-exit_on_time_min_runs=100000")
.inOrder();
}

@Test
void onlyMinRunsIsNotForwarded() {
assertThat(translate(0, 5, "--exit_on_time_min_runs=5")).isEmpty();
}

@Test
void explicitZeroIsNotForwarded() {
assertThat(translate(0, 5, "--exit_on_time=0", "--exit_on_time_min_runs=5")).isEmpty();
}

@Test
void nativeFlagsTakePrecedence() {
assertThat(
translate(
100,
5,
"--exit_on_time=100",
"-exit_on_time=2",
"--exit_on_time_min_runs=5",
"-exit_on_time_min_runs=7"))
.containsExactly("-exit_on_time=2", "-exit_on_time_min_runs=7")
.inOrder();
}

@Test
void unrelatedArgsArePreserved() {
assertThat(translate(0, 100000, "-runs=10", "--target_class=Foo"))
.containsExactly("-runs=10", "--target_class=Foo")
.inOrder();
}

@Test
void valuesExceedingIntRangeAreRejected() {
assertThrows(
IllegalArgumentException.class,
() -> translate((long) Integer.MAX_VALUE + 1, 1, "--exit_on_time=2147483648"));
assertThrows(
IllegalArgumentException.class,
() -> translate(1, (long) Integer.MAX_VALUE + 1, "--exit_on_time=1"));
// Values parsed as unsigned 64-bit integers may appear negative as signed longs.
assertThrows(IllegalArgumentException.class, () -> translate(-1, 1));
assertThat(translate(Integer.MAX_VALUE, Integer.MAX_VALUE))
.containsExactly("-exit_on_time=2147483647", "-exit_on_time_min_runs=2147483647")
.inOrder();
}
}
75 changes: 75 additions & 0 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,81 @@ java_fuzz_target_test(
],
)

# The native libFuzzer flags exit the run early.
java_fuzz_target_test(
name = "ExitOnTimeFuzzer",
srcs = ["src/test/java/com/example/ExitOnTimeFuzzer.java"],
env = {
"MAX_FUZZING_SECONDS": "30",
},
expect_non_crash_exit_code = 0,
fuzzer_args = [
"-exit_on_time=2",
"-exit_on_time_min_runs=1",
"-max_total_time=60",
],
target_class = "com.example.ExitOnTimeFuzzer",
verify_crash_input = False,
verify_crash_reproducer = False,
)

# The Jazzer options are translated into libFuzzer flags and not forwarded verbatim, which would
# result in a warning about an unknown flag.
java_fuzz_target_test(
name = "ExitOnTimeJazzerOptionFuzzer",
srcs = ["src/test/java/com/example/ExitOnTimeFuzzer.java"],
env = {
"MAX_FUZZING_SECONDS": "30",
},
expect_non_crash_exit_code = 0,
fuzzer_args = [
"--exit_on_time=2",
"--exit_on_time_min_runs=1",
"-max_total_time=60",
],
target_class = "com.example.ExitOnTimeFuzzer",
verify_crash_input = False,
verify_crash_reproducer = False,
)

# An unreachable number of minimum runs prevents --exit_on_time from triggering, so the run only
# stops at -max_total_time.
java_fuzz_target_test(
name = "ExitOnTimeMinRunsGateFuzzer",
srcs = ["src/test/java/com/example/ExitOnTimeFuzzer.java"],
env = {
"MIN_FUZZING_SECONDS": "4",
},
expect_non_crash_exit_code = 0,
fuzzer_args = [
"--exit_on_time=1",
"--exit_on_time_min_runs=2147483647",
"-max_total_time=5",
],
target_class = "com.example.ExitOnTimeFuzzer",
verify_crash_input = False,
verify_crash_reproducer = False,
)

# A native -exit_on_time flag takes precedence over --exit_on_time.
java_fuzz_target_test(
name = "ExitOnTimeNativeFlagPrecedenceFuzzer",
srcs = ["src/test/java/com/example/ExitOnTimeFuzzer.java"],
env = {
"MAX_FUZZING_SECONDS": "30",
},
expect_non_crash_exit_code = 0,
fuzzer_args = [
"--exit_on_time=100",
"-exit_on_time=2",
"--exit_on_time_min_runs=1",
"-max_total_time=60",
],
target_class = "com.example.ExitOnTimeFuzzer",
verify_crash_input = False,
verify_crash_reproducer = False,
)

java_fuzz_target_test(
name = "ForkModeFuzzer",
size = "enormous",
Expand Down
Loading