From 814d2c2bfee4bcb098dcaef7fccc473cb80a32fb Mon Sep 17 00:00:00 2001 From: "Sh. Alexander" Date: Tue, 8 Sep 2026 20:32:42 +0200 Subject: [PATCH 1/3] feat: add exit_on_time and exit_on_time_min_runs options --- MODULE.bazel | 2 + README.md | 18 +++ docs/arguments-and-configuration-options.md | 11 ++ .../jazzer/driver/Driver.java | 17 +++ .../code_intelligence/jazzer/driver/Opt.java | 12 ++ .../jazzer/junit/FuzzTestExecutor.java | 3 + tests/BUILD.bazel | 10 ++ .../java/com/example/ExitOnTimeFuzzer.java | 22 ++++ third_party/libfuzzer-exit-on-time.patch | 118 ++++++++++++++++++ 9 files changed, 213 insertions(+) create mode 100644 tests/src/test/java/com/example/ExitOnTimeFuzzer.java create mode 100644 third_party/libfuzzer-exit-on-time.patch diff --git a/MODULE.bazel b/MODULE.bazel index a759ec2c1..12b9d208c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -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", diff --git a/README.md b/README.md index 7d91c3f3e..2cffc343b 100644 --- a/README.md +++ b/README.md @@ -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=`** - 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=`** (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. diff --git a/docs/arguments-and-configuration-options.md b/docs/arguments-and-configuration-options.md index 16e7c5183..7d5d73779 100644 --- a/docs/arguments-and-configuration-options.md +++ b/docs/arguments-and-configuration-options.md @@ -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: diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java index 30e7a2418..73e2e3a96 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -173,6 +173,23 @@ public static int start(List args, boolean spawnsSubprocesses) throws IO args.add("-runs=" + Opt.maxExecutions.get()); } } + if (Opt.exitOnTime.get() > 0) { + // 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=")); + // Opt has already consumed this Jazzer-specific option. Do not forward it to libFuzzer, + // which only accepts the single-dash spelling and would otherwise emit a warning. + args.removeIf(a -> a.startsWith("--exit_on_time=")); + if (!hasExitOnTime) { + args.add("-exit_on_time=" + Opt.exitOnTime.get()); + } + boolean hasExitOnTimeMinRuns = + args.stream().anyMatch(a -> a.startsWith("-exit_on_time_min_runs=")); + args.removeIf(a -> a.startsWith("--exit_on_time_min_runs=")); + if (!hasExitOnTimeMinRuns) { + args.add("-exit_on_time_min_runs=" + Opt.exitOnTimeMinRuns.get()); + } + } // Installing the agent after the following "findFuzzTarget" leads to an asan error // in it on "Class.forName(targetClassName)", but only during native fuzzing. diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java index 16cd022a6..768de2ee0 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -130,6 +130,18 @@ public final class Opt { "Glob patterns matching names of classes to instrument with hooks (custom and built-in)"); public static final OptItem dedup = boolSetting("dedup", true, "Compute and print a deduplication token for every finding"); + public static final OptItem 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). Single-process fuzzing only."); + public static final OptItem exitOnTimeMinRuns = + uint64Setting( + "exit_on_time_min_runs", + 100000, + "Minimum number of fuzzer runs before --exit_on_time is allowed to trigger" + + " (default: 100000)."); public static final OptItem> disabledHooks = stringListSetting( "disabled_hooks", diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java index a5fe9baff..6b2371dbe 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -120,6 +120,9 @@ public static FuzzTestExecutor prepare(ExtensionContext context, Optional if (Opt.maxExecutions.get() > 0) { libFuzzerArgs.add("-runs=" + Opt.maxExecutions.get()); } + if (Opt.exitOnTime.get() > 0) { + libFuzzerArgs.add("-exit_on_time=" + Opt.exitOnTime.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. diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index bd857bfd2..9dfd6d237 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -73,6 +73,16 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "ExitOnTimeFuzzer", + srcs = ["src/test/java/com/example/ExitOnTimeFuzzer.java"], + expect_non_crash_exit_code = 0, + fuzzer_args = ["-exit_on_time=2"], + target_class = "com.example.ExitOnTimeFuzzer", + verify_crash_input = False, + verify_crash_reproducer = False, +) + java_fuzz_target_test( name = "ForkModeFuzzer", size = "enormous", diff --git a/tests/src/test/java/com/example/ExitOnTimeFuzzer.java b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java new file mode 100644 index 000000000..7ef0602f7 --- /dev/null +++ b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java @@ -0,0 +1,22 @@ +/* + * 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.example; + +/** A target with no input-dependent coverage, used to test idle coverage limits. */ +public final class ExitOnTimeFuzzer { + public static void fuzzerTestOneInput(byte[] ignored) {} +} diff --git a/third_party/libfuzzer-exit-on-time.patch b/third_party/libfuzzer-exit-on-time.patch new file mode 100644 index 000000000..15e17cd2e --- /dev/null +++ b/third_party/libfuzzer-exit-on-time.patch @@ -0,0 +1,118 @@ +diff --git a/FuzzerFlags.def b/FuzzerFlags.def +--- a/FuzzerFlags.def ++++ b/FuzzerFlags.def +@@ -55,6 +55,12 @@ + "this exit code will be used.") + FUZZER_FLAG_INT(max_total_time, 0, "If positive, indicates the maximal total " + "time in seconds to run the fuzzer.") ++FUZZER_FLAG_INT(exit_on_time, 0, ++ "Exit successfully if no new coverage features are found for " ++ "this many seconds (0 disables the limit).") ++FUZZER_FLAG_INT(exit_on_time_min_runs, 100000, ++ "Minimum number of fuzzer runs before -exit_on_time is " ++ "allowed to trigger (default: 100000).") + FUZZER_FLAG_INT(help, 0, "Print help.") + FUZZER_FLAG_INT(fork, 0, "Experimental mode where fuzzing happens " + "in a subprocess") +diff --git a/FuzzerOptions.h b/FuzzerOptions.h +--- a/FuzzerOptions.h ++++ b/FuzzerOptions.h +@@ -28,6 +28,8 @@ + bool IgnoreOOMs = true; + bool IgnoreCrashes = false; + int MaxTotalTimeSec = 0; ++ int ExitOnTimeSec = 0; ++ int ExitOnTimeMinRuns = 100000; + int RssLimitMb = 0; + int MallocLimitMb = 0; + bool DoCrossOver = true; +diff --git a/FuzzerInternal.h b/FuzzerInternal.h +--- a/FuzzerInternal.h ++++ b/FuzzerInternal.h +@@ -50,6 +50,13 @@ + static_cast(Options.MaxTotalTimeSec); + } + ++ bool ExitOnTimeReached() { ++ return Options.ExitOnTimeSec > 0 && ++ TotalNumberOfRuns >= (size_t)Options.ExitOnTimeMinRuns && ++ duration_cast(system_clock::now() - LastNewCoverageTime) ++ .count() >= Options.ExitOnTimeSec; ++ } ++ + size_t execPerSec() { + size_t Seconds = secondsSinceProcessStartUp(); + return Seconds ? TotalNumberOfRuns / Seconds : 0; +@@ -137,6 +143,7 @@ + FuzzingOptions Options; + DataFlowTrace DFT; + ++ system_clock::time_point LastNewCoverageTime; + system_clock::time_point ProcessStartTime = system_clock::now(); + system_clock::time_point UnitStartTime, UnitStopTime; + long TimeOfLongestUnitInSeconds = 0; +diff --git a/FuzzerLoop.cpp b/FuzzerLoop.cpp +--- a/FuzzerLoop.cpp ++++ b/FuzzerLoop.cpp +@@ -535,6 +535,8 @@ + *FoundUniqFeatures = FoundUniqFeaturesOfII; + PrintPulseAndReportSlowInput(Data, Size); + size_t NumNewFeatures = Corpus.NumFeatureUpdates() - NumUpdatesBefore; ++ if (NumNewFeatures > 0) ++ LastNewCoverageTime = system_clock::now(); + if (NumNewFeatures || ForceAddToCorpus) { + TPC.UpdateObservedPCs(); + auto NewII = +@@ -862,6 +864,7 @@ + TPC.SetPrintNewPCs(Options.PrintNewCovPcs); + TPC.SetPrintNewFuncs(Options.PrintNewCovFuncs); + system_clock::time_point LastCorpusReload = system_clock::now(); ++ LastNewCoverageTime = system_clock::now(); + + TmpMaxMutationLen = + Min(MaxMutationLen, Max(size_t(4), Corpus.MaxInputSize())); +@@ -880,6 +883,11 @@ + break; + if (TimedOut()) + break; ++ if (ExitOnTimeReached()) { ++ Printf("INFO: no new coverage features for %d seconds; exiting.\n", ++ Options.ExitOnTimeSec); ++ break; ++ } + + // Update TmpMaxMutationLen + if (Options.LenControl) { +diff --git a/FuzzerDriver.cpp b/FuzzerDriver.cpp +--- a/FuzzerDriver.cpp ++++ b/FuzzerDriver.cpp +@@ -665,6 +665,20 @@ + if (Flags.close_fd_mask & 1) + CloseStdout(); + ++ if (Flags.exit_on_time < 0) { ++ Printf("ERROR: -exit_on_time must be non-negative\n"); ++ return 1; ++ } ++ if (Flags.exit_on_time_min_runs < 0) { ++ Printf("ERROR: -exit_on_time_min_runs must be non-negative\n"); ++ return 1; ++ } ++ if (Flags.exit_on_time > 0 && ++ (Flags.fork || Flags.jobs || Flags.merge || Flags.set_cover_merge)) { ++ Printf("ERROR: -exit_on_time is only supported for single-process fuzzing\n"); ++ return 1; ++ } ++ + if (Flags.jobs > 0 && Flags.workers == 0) { + Flags.workers = std::min(NumberOfCpuCores() / 2, Flags.jobs); + if (Flags.workers > 1) +@@ -686,6 +696,8 @@ + Options.IgnoreOOMs = Flags.ignore_ooms; + Options.IgnoreCrashes = Flags.ignore_crashes; + Options.MaxTotalTimeSec = Flags.max_total_time; ++ Options.ExitOnTimeSec = Flags.exit_on_time; ++ Options.ExitOnTimeMinRuns = Flags.exit_on_time_min_runs; + Options.DoCrossOver = Flags.cross_over; + Options.CrossOverUniformDist = Flags.cross_over_uniform_dist; + Options.MutateDepth = Flags.mutate_depth; From a084f0811f8dff0c13e9c6fafd0cee13c2cd700a Mon Sep 17 00:00:00 2001 From: Alexander <60114847+sigdevel@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:13:09 +0300 Subject: [PATCH 2/3] fix: improve exit_on_time option handling and test coverage --- .../jazzer/driver/BUILD.bazel | 1 + .../jazzer/driver/Driver.java | 60 ++++++++---- .../code_intelligence/jazzer/driver/Opt.java | 5 +- .../jazzer/junit/FuzzTestExecutor.java | 17 +++- .../jazzer/driver/BUILD.bazel | 10 ++ .../jazzer/driver/DriverTest.java | 93 +++++++++++++++++++ tests/BUILD.bazel | 67 ++++++++++++- .../java/com/example/ExitOnTimeFuzzer.java | 28 +++++- third_party/libfuzzer-exit-on-time.patch | 2 +- 9 files changed, 261 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/code_intelligence/jazzer/driver/DriverTest.java diff --git a/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel index a8b91bf67..2500223c4 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -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", diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java index 73e2e3a96..bafcc82a1 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -173,22 +173,11 @@ public static int start(List args, boolean spawnsSubprocesses) throws IO args.add("-runs=" + Opt.maxExecutions.get()); } } - if (Opt.exitOnTime.get() > 0) { - // 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=")); - // Opt has already consumed this Jazzer-specific option. Do not forward it to libFuzzer, - // which only accepts the single-dash spelling and would otherwise emit a warning. - args.removeIf(a -> a.startsWith("--exit_on_time=")); - if (!hasExitOnTime) { - args.add("-exit_on_time=" + Opt.exitOnTime.get()); - } - boolean hasExitOnTimeMinRuns = - args.stream().anyMatch(a -> a.startsWith("-exit_on_time_min_runs=")); - args.removeIf(a -> a.startsWith("--exit_on_time_min_runs=")); - if (!hasExitOnTimeMinRuns) { - args.add("-exit_on_time_min_runs=" + Opt.exitOnTimeMinRuns.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 @@ -198,6 +187,45 @@ public static int start(List 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 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 diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java index 768de2ee0..549c89613 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -135,13 +135,14 @@ public final class Opt { "exit_on_time", 0, "Exit fuzzing successfully if no new coverage features are discovered for the specified" - + " number of seconds (0 disables the limit). Single-process fuzzing only."); + + " number of seconds (0 disables the limit, at most 2147483647). Single-process" + + " fuzzing only."); public static final OptItem exitOnTimeMinRuns = uint64Setting( "exit_on_time_min_runs", 100000, "Minimum number of fuzzer runs before --exit_on_time is allowed to trigger" - + " (default: 100000)."); + + " (default: 100000, at most 2147483647)."); public static final OptItem> disabledHooks = stringListSetting( "disabled_hooks", diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java index 6b2371dbe..c09f61a7f 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -121,7 +121,11 @@ public static FuzzTestExecutor prepare(ExtensionContext context, Optional libFuzzerArgs.add("-runs=" + Opt.maxExecutions.get()); } if (Opt.exitOnTime.get() > 0) { - libFuzzerArgs.add("-exit_on_time=" + Opt.exitOnTime.get()); + 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 @@ -140,6 +144,17 @@ public static FuzzTestExecutor prepare(ExtensionContext context, Optional 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 translateJUnitTimeoutToLibFuzzerFlag(ExtensionContext context) { return Stream.>>of( () -> diff --git a/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel index 4398eda58..6cc9420a8 100644 --- a/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -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"], diff --git a/src/test/java/com/code_intelligence/jazzer/driver/DriverTest.java b/src/test/java/com/code_intelligence/jazzer/driver/DriverTest.java new file mode 100644 index 000000000..0f267a906 --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/driver/DriverTest.java @@ -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 translate(long exitOnTime, long exitOnTimeMinRuns, String... args) { + List 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(); + } +} diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 9dfd6d237..5c6d8f397 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -73,11 +73,76 @@ 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=2"], + 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, diff --git a/tests/src/test/java/com/example/ExitOnTimeFuzzer.java b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java index 7ef0602f7..6b4b9d58a 100644 --- a/tests/src/test/java/com/example/ExitOnTimeFuzzer.java +++ b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java @@ -16,7 +16,33 @@ package com.example; -/** A target with no input-dependent coverage, used to test idle coverage limits. */ +/** + * A target with no input-dependent coverage, used to test idle coverage limits. + * + *

The optional environment variables {@code MIN_FUZZING_SECONDS} and {@code + * MAX_FUZZING_SECONDS} bound the duration of the fuzzing run, which distinguishes an exit caused + * by -exit_on_time from one caused by -max_total_time. + */ public final class ExitOnTimeFuzzer { + private static long startNanos; + + public static void fuzzerInitialize() { + startNanos = System.nanoTime(); + } + public static void fuzzerTestOneInput(byte[] ignored) {} + + public static void fuzzerTearDown() { + long elapsedSeconds = (System.nanoTime() - startNanos) / 1_000_000_000L; + String min = System.getenv("MIN_FUZZING_SECONDS"); + if (min != null && elapsedSeconds < Long.parseLong(min)) { + throw new IllegalStateException( + "Fuzzing stopped after " + elapsedSeconds + "s, expected at least " + min + "s"); + } + String max = System.getenv("MAX_FUZZING_SECONDS"); + if (max != null && elapsedSeconds > Long.parseLong(max)) { + throw new IllegalStateException( + "Fuzzing stopped after " + elapsedSeconds + "s, expected at most " + max + "s"); + } + } } diff --git a/third_party/libfuzzer-exit-on-time.patch b/third_party/libfuzzer-exit-on-time.patch index 15e17cd2e..564d841e7 100644 --- a/third_party/libfuzzer-exit-on-time.patch +++ b/third_party/libfuzzer-exit-on-time.patch @@ -35,7 +35,7 @@ diff --git a/FuzzerInternal.h b/FuzzerInternal.h + bool ExitOnTimeReached() { + return Options.ExitOnTimeSec > 0 && -+ TotalNumberOfRuns >= (size_t)Options.ExitOnTimeMinRuns && ++ TotalNumberOfRuns >= static_cast(Options.ExitOnTimeMinRuns) && + duration_cast(system_clock::now() - LastNewCoverageTime) + .count() >= Options.ExitOnTimeSec; + } From 7f72651cd431e8769fa292c7ab0844d38452f3bd Mon Sep 17 00:00:00 2001 From: Alexander <60114847+sigdevel@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:02:05 +0300 Subject: [PATCH 3/3] style: apply format.sh --- .../java/com/code_intelligence/jazzer/driver/Driver.java | 3 +-- tests/src/test/java/com/example/ExitOnTimeFuzzer.java | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java index bafcc82a1..16055586a 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -196,8 +196,7 @@ static void translateExitOnTimeOptions( List 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=")); + args.removeIf(a -> a.startsWith("--exit_on_time=") || a.startsWith("--exit_on_time_min_runs=")); if (exitOnTime == 0) { return; } diff --git a/tests/src/test/java/com/example/ExitOnTimeFuzzer.java b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java index 6b4b9d58a..bead61cff 100644 --- a/tests/src/test/java/com/example/ExitOnTimeFuzzer.java +++ b/tests/src/test/java/com/example/ExitOnTimeFuzzer.java @@ -19,9 +19,9 @@ /** * A target with no input-dependent coverage, used to test idle coverage limits. * - *

The optional environment variables {@code MIN_FUZZING_SECONDS} and {@code - * MAX_FUZZING_SECONDS} bound the duration of the fuzzing run, which distinguishes an exit caused - * by -exit_on_time from one caused by -max_total_time. + *

The optional environment variables {@code MIN_FUZZING_SECONDS} and {@code MAX_FUZZING_SECONDS} + * bound the duration of the fuzzing run, which distinguishes an exit caused by -exit_on_time from + * one caused by -max_total_time. */ public final class ExitOnTimeFuzzer { private static long startNanos;