diff --git a/docs/common.md b/docs/common.md index d65832d6a..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 @@ -34,6 +35,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/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/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 66befa431..45ae9f1a5 100644 --- a/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java +++ b/src/main/java/com/code_intelligence/jazzer/junit/FuzzTestExecutor.java @@ -17,10 +17,10 @@ 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; +import static com.code_intelligence.jazzer.utils.Utils.durationStringToSeconds; import com.code_intelligence.jazzer.agent.AgentInstaller; import com.code_intelligence.jazzer.driver.FuzzTargetHolder; @@ -64,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( @@ -116,9 +115,9 @@ public static FuzzTestExecutor prepare( dictionaryPath.ifPresent(s -> libFuzzerArgs.add("-dict=" + s)); - libFuzzerArgs.add("-max_total_time=" + 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 @@ -266,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); } 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); + } +}