From 92464a1e5d4ce4533474d6851f0020408ae67088 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Fri, 26 Sep 2025 10:32:38 +0200 Subject: [PATCH 1/3] refactor: move duration parsing to common Utils class --- .../jazzer/junit/BUILD.bazel | 1 + .../jazzer/junit/FuzzTestExecutor.java | 5 +-- .../code_intelligence/jazzer/junit/Utils.java | 14 -------- .../code_intelligence/jazzer/utils/Utils.kt | 19 ++++++++++ .../jazzer/junit/UtilsTest.java | 13 ------- .../jazzer/utils/BUILD.bazel | 13 ++++++- .../jazzer/utils/UtilsTest.java | 36 +++++++++++++++++++ 7 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 src/test/java/com/code_intelligence/jazzer/utils/UtilsTest.java diff --git a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel index 613862316..374934dde 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/junit/BUILD.bazel @@ -110,6 +110,7 @@ java_jni_library( ":utils", "//deploy:jazzer", "//deploy:jazzer-api", + "//src/main/java/com/code_intelligence/jazzer/utils", "@maven//:jakarta_servlet_jakarta_servlet_api", "@maven//:org_junit_jupiter_junit_jupiter_api", "@maven//:org_junit_jupiter_junit_jupiter_params", 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 66befa431..171636522 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -17,7 +17,6 @@ package com.code_intelligence.jazzer.junit; import static com.code_intelligence.jazzer.junit.ApiStatsHolder.printApiStats; -import static com.code_intelligence.jazzer.junit.Utils.durationStringToSeconds; import static com.code_intelligence.jazzer.junit.Utils.generatedCorpusPath; import static com.code_intelligence.jazzer.junit.Utils.inputsDirectoryResourcePath; import static com.code_intelligence.jazzer.junit.Utils.inputsDirectorySourcePath; @@ -116,7 +115,9 @@ public static FuzzTestExecutor prepare( dictionaryPath.ifPresent(s -> libFuzzerArgs.add("-dict=" + s)); - libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(maxDuration)); + libFuzzerArgs.add( + "-max_total_time=" + + com.code_intelligence.jazzer.utils.Utils.durationStringToSeconds(maxDuration)); if (maxRuns > 0) { libFuzzerArgs.add("-runs=" + maxRuns); } diff --git a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java index 8794e4dc5..376752c25 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/Utils.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/Utils.java @@ -41,7 +41,6 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -277,19 +276,6 @@ static List getCorpusFilesOrDirs(ExtensionContext context) { .collect(toList()); } - /** - * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not - * allow for duration units longer than hours, so we can always prepend PT. - */ - static long durationStringToSeconds(String duration) { - if (duration.isEmpty()) { - return 0; - } - String isoDuration = - "PT" + duration.replace("sec", "s").replace("min", "m").replace("hr", "h").replace(" ", ""); - return Duration.parse(isoDuration).getSeconds(); - } - static long parseJUnitTimeoutValueToSeconds(String value) { Matcher matcher = DURATION_PATTERN.matcher(value); if (!matcher.matches()) { diff --git a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt index b9ec1a974..8af680fab 100644 --- a/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt +++ b/src/main/java/com/code_intelligence/jazzer/utils/Utils.kt @@ -18,6 +18,7 @@ package com.code_intelligence.jazzer.utils import java.lang.reflect.Executable +import java.time.Duration val Class<*>.readableDescriptor: String get() = @@ -47,3 +48,21 @@ val Executable.readableDescriptor: String parameterTypes.joinToString(separator = ",", prefix = "(", postfix = ")") { parameterType -> parameterType.readableDescriptor } + +/** + * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not + * allow for duration units longer than hours, so we can always prepend PT. + */ +fun durationStringToSeconds(duration: String): Long { + if (duration.isEmpty()) { + return 0 + } + val isoDuration = + "PT" + + duration + .replace("sec", "s") + .replace("min", "m") + .replace("hr", "h") + .replace(" ", "") + return Duration.parse(isoDuration).seconds +} diff --git a/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java b/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java index 02349ea64..10a2dbad6 100644 --- a/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java +++ b/src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java @@ -16,7 +16,6 @@ package com.code_intelligence.jazzer.junit; -import static com.code_intelligence.jazzer.junit.Utils.durationStringToSeconds; import static com.code_intelligence.jazzer.junit.Utils.getMarkedArguments; import static com.code_intelligence.jazzer.junit.Utils.getMarkedInstance; import static com.code_intelligence.jazzer.junit.Utils.isMarkedInstance; @@ -58,18 +57,6 @@ public class UtilsTest implements InvocationInterceptor { @TempDir Path temp; - @Test - void testDurationStringToSeconds() { - assertThat(durationStringToSeconds("")).isEqualTo(0); - assertThat(durationStringToSeconds("0s")).isEqualTo(0); - assertThat(durationStringToSeconds("1m")).isEqualTo(60); - assertThat(durationStringToSeconds("1min")).isEqualTo(60); - assertThat(durationStringToSeconds("1h")).isEqualTo(60 * 60); - assertThat(durationStringToSeconds("1h 2m 30s")).isEqualTo(60 * 60 + 2 * 60 + 30); - assertThat(durationStringToSeconds("1hr2min30sec")).isEqualTo(60 * 60 + 2 * 60 + 30); - assertThat(durationStringToSeconds("1h2m30s")).isEqualTo(60 * 60 + 2 * 60 + 30); - } - @Test void testParseJUnitTimeoutValueToSeconds() { assertThat(parseJUnitTimeoutValueToSeconds("5")).isEqualTo(5); diff --git a/src/test/java/com/code_intelligence/jazzer/utils/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/utils/BUILD.bazel index e78fcef17..cd98bf1b3 100644 --- a/src/test/java/com/code_intelligence/jazzer/utils/BUILD.bazel +++ b/src/test/java/com/code_intelligence/jazzer/utils/BUILD.bazel @@ -1,4 +1,4 @@ -load("//bazel:compat.bzl", "SKIP_ON_WINDOWS") +load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_junit5_test") java_library( name = "test_utils", @@ -8,3 +8,14 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/utils:log", ], ) + +java_junit5_test( + name = "UtilsTest", + size = "small", + srcs = ["UtilsTest.java"], + deps = JUNIT5_DEPS + [ + "//src/main/java/com/code_intelligence/jazzer/utils", + "@maven//:com_google_truth_truth", + "@maven//:org_junit_jupiter_junit_jupiter_api", + ], +) diff --git a/src/test/java/com/code_intelligence/jazzer/utils/UtilsTest.java b/src/test/java/com/code_intelligence/jazzer/utils/UtilsTest.java new file mode 100644 index 000000000..ea4575bfe --- /dev/null +++ b/src/test/java/com/code_intelligence/jazzer/utils/UtilsTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.code_intelligence.jazzer.utils; + +import static com.code_intelligence.jazzer.utils.Utils.durationStringToSeconds; +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; + +public class UtilsTest { + @Test + void testDurationStringToSeconds() { + assertThat(durationStringToSeconds("")).isEqualTo(0); + assertThat(durationStringToSeconds("0s")).isEqualTo(0); + assertThat(durationStringToSeconds("1m")).isEqualTo(60); + assertThat(durationStringToSeconds("1min")).isEqualTo(60); + assertThat(durationStringToSeconds("1h")).isEqualTo(60 * 60); + assertThat(durationStringToSeconds("1h 2m 30s")).isEqualTo(60 * 60 + 2 * 60 + 30); + assertThat(durationStringToSeconds("1hr2min30sec")).isEqualTo(60 * 60 + 2 * 60 + 30); + assertThat(durationStringToSeconds("1h2m30s")).isEqualTo(60 * 60 + 2 * 60 + 30); + } +} From 5d8ce0701597beeb70e18f6991a87c7376981497 Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Fri, 26 Sep 2025 10:52:03 +0200 Subject: [PATCH 2/3] feat: add options for max fuzzing duration / runs The main purpose is to improve the usability of configuring fuzzing duration at runtime when using the JUnit integration. Since passing libFuzzer args is not very ergonomic the dedicated options add the convenience of running e.g. `JAZZER_MAX_DURATION=10m mvn ...` to change the fuzzing duration. --- docs/common.md | 9 +++++++++ .../jazzer/driver/BUILD.bazel | 1 + .../jazzer/driver/Driver.java | 15 +++++++++++++++ .../code_intelligence/jazzer/driver/Opt.java | 14 ++++++++++++++ .../jazzer/junit/AgentConfigurator.java | 8 +++++++- .../jazzer/junit/FuzzTest.java | 4 ++++ .../jazzer/junit/FuzzTestExecutor.java | 19 +++++++++---------- 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/docs/common.md b/docs/common.md index d65832d6a..61f04f743 100644 --- a/docs/common.md +++ b/docs/common.md @@ -34,6 +34,15 @@ The value of a setting item `some_opt` is obtained from the following sources in - the `jazzer.some_opt` JUnit configuration parameter - the `--some_opt` CLI parameter +## Global Time/Execution Limits + +- `jazzer.max_duration`: Sets the maximum fuzzing duration (e.g., `30s`, `2m`, `1h`; empty = unlimited). + - Standalone: translated to libFuzzer flag `-max_total_time` unless that flag is already set. + - JUnit: overrides `@FuzzTest(maxDuration)`. +- `jazzer.max_executions`: Sets the maximum number of executions (positive value limits runs). + - Standalone: translated to libFuzzer flag `-runs` unless that flag is already set. + - JUnit: overrides `@FuzzTest(maxExecutions)`. + ### Reproducing a finding When Jazzer manages to find an input that causes an uncaught exception or a failed assertion, it prints a Java stack trace and creates two files that aid in reproducing the crash without Jazzer: 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 6c77c2ac5..a8b91bf67 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel +++ b/src/main/java/com/code_intelligence/jazzer/driver/BUILD.bazel @@ -24,6 +24,7 @@ java_library( "//src/main/java/com/code_intelligence/jazzer/android:android_runtime", "//src/main/java/com/code_intelligence/jazzer/driver/junit:junit_runner", "//src/main/java/com/code_intelligence/jazzer/runtime:constants", + "//src/main/java/com/code_intelligence/jazzer/utils", "//src/main/java/com/code_intelligence/jazzer/utils:log", ], ) 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 8d50b1a6c..30e7a2418 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Driver.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Driver.java @@ -23,6 +23,7 @@ import com.code_intelligence.jazzer.agent.AgentInstaller; import com.code_intelligence.jazzer.driver.junit.JUnitRunner; import com.code_intelligence.jazzer.utils.Log; +import com.code_intelligence.jazzer.utils.Utils; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -159,6 +160,20 @@ public static int start(List args, boolean spawnsSubprocesses) throws IO } } + // Translate Jazzer options to libFuzzer flags if not explicitly provided. + if (Opt.maxDuration.isSet() && !Opt.maxDuration.get().isEmpty()) { + boolean hasMaxTime = args.stream().anyMatch(a -> a.startsWith("-max_total_time=")); + if (!hasMaxTime) { + args.add("-max_total_time=" + Utils.durationStringToSeconds(Opt.maxDuration.get())); + } + } + if (Opt.maxExecutions.isSet() && Opt.maxExecutions.get() > 0) { + boolean hasRuns = args.stream().anyMatch(a -> a.startsWith("-runs=")); + if (!hasRuns) { + args.add("-runs=" + Opt.maxExecutions.get()); + } + } + // Installing the agent after the following "findFuzzTarget" leads to an asan error // in it on "Class.forName(targetClassName)", but only during native fuzzing. AgentInstaller.install(Opt.hooks.get()); 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 5840a7c19..ea82af613 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -205,6 +205,20 @@ public final class Opt { "", "Fully qualified name of the fuzz target class (required unless --autofuzz is" + " specified)"); + public static final OptItem maxDuration = + stringSetting( + "max_duration", + "", + "Sets the maximum fuzzing duration (e.g., '30s', '2m', '1h'; empty = unlimited). For" + + " JUnit tests, overrides @FuzzTest(maxDuration); for standalone, translates to" + + " -max_total_time unless that flag is already present."); + public static final OptItem maxExecutions = + uint64Setting( + "max_executions", + 0, + "Sets the maximum number of executions during fuzzing (positive value limits runs). For" + + " JUnit tests, overrides @FuzzTest(maxExecutions); for standalone, translates to" + + " -runs unless that flag is already present."); // Used to disambiguate between multiple methods annotated with @FuzzTest in the target class. public static final OptItem targetMethod = stringSetting( diff --git a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java index fe758141a..37984148d 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/AgentConfigurator.java @@ -45,13 +45,19 @@ static void forRegressionTest(ExtensionContext extensionContext) { Opt.customHookIncludes.setIfDefault(Opt.instrument.get()); } - static void forFuzzing(ExtensionContext extensionContext) { + static void forFuzzing( + ExtensionContext extensionContext, String fuzzingDuration, long maxExecutions) { if (!hasBeenConfigured.compareAndSet(false, true)) { throw new IllegalStateException("Only a single fuzz test should be executed per fuzzing run"); } applyCommonConfiguration(extensionContext); + // Allow dynamic overrides via Opt while keeping annotation values as defaults. + // If the user supplied jazzer.max_duration/JAZZER_MAX_DURATION/--max_duration, it will win. + Opt.maxDuration.setIfDefault(fuzzingDuration); + Opt.maxExecutions.setIfDefault(maxExecutions); + Opt.instrument.setIfDefault(determineInstrumentationFilters(extensionContext)); Opt.customHookIncludes.setIfDefault(Opt.instrument.get()); Opt.instrumentationIncludes.setIfDefault(Opt.instrument.get()); diff --git a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java index aec397520..f2026bd5b 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTest.java @@ -113,6 +113,8 @@ *

To remove the default limit of 5 minutes, set this element to {@code ""}. * *

This option has no effect during regression testing. + * + *

This value can be overridden at runtime via the Jazzer option {@code jazzer.max_duration}. */ String maxDuration() default "5m"; @@ -124,6 +126,8 @@ * fuzzing across machine's with different performance characteristics. * *

This option has no effect during regression testing. + * + *

This value can be overridden at runtime via the Jazzer option {@code jazzer.max_executions}. */ long maxExecutions() default 0; 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 171636522..45ae9f1a5 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -20,6 +20,7 @@ import static com.code_intelligence.jazzer.junit.Utils.generatedCorpusPath; import static com.code_intelligence.jazzer.junit.Utils.inputsDirectoryResourcePath; import static com.code_intelligence.jazzer.junit.Utils.inputsDirectorySourcePath; +import static com.code_intelligence.jazzer.utils.Utils.durationStringToSeconds; import com.code_intelligence.jazzer.agent.AgentInstaller; import com.code_intelligence.jazzer.driver.FuzzTargetHolder; @@ -63,8 +64,7 @@ private FuzzTestExecutor(List libFuzzerArgs, Optional javaSeedsDir this.javaSeedsDir = javaSeedsDir; } - public static FuzzTestExecutor prepare( - ExtensionContext context, String maxDuration, long maxRuns, Optional dictionaryPath) + public static FuzzTestExecutor prepare(ExtensionContext context, Optional dictionaryPath) throws IOException { if (!hasBeenPrepared.compareAndSet(false, true)) { throw new FuzzTestConfigurationError( @@ -115,11 +115,9 @@ public static FuzzTestExecutor prepare( dictionaryPath.ifPresent(s -> libFuzzerArgs.add("-dict=" + s)); - libFuzzerArgs.add( - "-max_total_time=" - + com.code_intelligence.jazzer.utils.Utils.durationStringToSeconds(maxDuration)); - if (maxRuns > 0) { - libFuzzerArgs.add("-runs=" + maxRuns); + libFuzzerArgs.add("-max_total_time=" + durationStringToSeconds(Opt.maxDuration.get())); + if (Opt.maxExecutions.get() > 0) { + libFuzzerArgs.add("-runs=" + Opt.maxExecutions.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 @@ -267,10 +265,11 @@ static void configureAndInstallAgent( return; } if (Utils.isFuzzing(extensionContext)) { - FuzzTestExecutor executor = - prepare(extensionContext, maxDuration, maxExecutions, dictionaryPath); + // Register configuration parameters and apply common config before reading Opt values + // inside prepare(...), so JUnit config (junit-platform.properties) can override defaults. + AgentConfigurator.forFuzzing(extensionContext, maxDuration, maxExecutions); + FuzzTestExecutor executor = prepare(extensionContext, dictionaryPath); extensionContext.getRoot().getStore(Namespace.GLOBAL).put(FuzzTestExecutor.class, executor); - AgentConfigurator.forFuzzing(extensionContext); } else { AgentConfigurator.forRegressionTest(extensionContext); } From 908091d5a59de85ed1cef727872c7b8aa3dae7ed Mon Sep 17 00:00:00 2001 From: Simon Resch Date: Fri, 26 Sep 2025 13:04:31 +0200 Subject: [PATCH 3/3] docs: adjust note about options in JUnit integration --- docs/common.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/common.md b/docs/common.md index 61f04f743..14fbebdd6 100644 --- a/docs/common.md +++ b/docs/common.md @@ -7,7 +7,8 @@ * [Autofuzz Mode](#autofuzz-mode) > [!NOTE]\ -> These settings apply to the old fuzzing approach using a `fuzzerTestOneInput` method and the native Jazzer binary. They don't work in the new JUnit integration. +> This page focuses on standalone/CLI usage. The JUnit integration uses the same precedence model, however some options +> are not used when executing fuzz tests via the JUnit integration. ## Recommended JVM Options