diff --git a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt index 4ff0fb1ad..4a6ac6ee1 100644 --- a/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt +++ b/src/main/java/com/code_intelligence/jazzer/agent/Agent.kt @@ -50,6 +50,17 @@ fun installInternal( dumpClassesDir: String = Opt.dumpClassesDir.get(), additionalClassesExcludes: List = Opt.additionalClassesExcludes.get(), ) { + // Enable conditional hooks when custom hooks are used together with coverage reporting so + // that hooks can be disabled during coverage report generation at shutdown (see #878). + // Without this, any use of hooked classes during coverage dumping would trigger custom hook + // dispatch, causing NoClassDefFoundError when the hook class is no longer loadable. + val useConditionalHooks = + conditionalHooks || + ( + userHookNames.isNotEmpty() && + (Opt.coverageDump.get().isNotEmpty() || Opt.coverageReport.get().isNotEmpty()) + ) + val allCustomHookNames = (Constants.SANITIZER_HOOK_NAMES + userHookNames).toSet() check(allCustomHookNames.isNotEmpty()) { "No hooks registered; expected at least the built-in hooks" } val customHookNames = allCustomHookNames - disabledHookNames.toSet() @@ -136,7 +147,7 @@ fun installInternal( instrumentationTypes, includedHooks.hooks, customHooks.hooks, - conditionalHooks, + useConditionalHooks, customHooks.additionalHookClassNameGlobber, coverageIdSynchronizer, dumpClassesDirPath, diff --git a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java index 63b300114..a4461a0fa 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/FuzzTargetRunner.java @@ -448,6 +448,7 @@ public static void registerFatalFindingHandlerForJUnit(Consumer findi private static void shutdown() { if (!Opt.coverageDump.get().isEmpty() || !Opt.coverageReport.get().isEmpty()) { + JazzerInternal.hooksEnabled = false; if (!Opt.coverageDump.get().isEmpty()) { CoverageRecorder.dumpJacocoCoverage(Opt.coverageDump.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 ea82af613..16cd022a6 100644 --- a/src/main/java/com/code_intelligence/jazzer/driver/Opt.java +++ b/src/main/java/com/code_intelligence/jazzer/driver/Opt.java @@ -250,7 +250,8 @@ public final class Opt { public static final OptItem mergeInner = boolSetting("merge_inner", false, null); // Whether hook instrumentation should add a check for JazzerInternal#hooksEnabled before - // executing hooks. Used to disable hooks during non-fuzz JUnit tests. + // executing hooks. Used to disable hooks during non-fuzz JUnit tests and during coverage + // report generation at shutdown. public static final OptItem conditionalHooks = boolSetting("conditional_hooks", false, null); diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 2eb9b778f..ff61e6369 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -124,6 +124,32 @@ java_fuzz_target_test( ], ) +java_binary( + name = "CoverageWithHooksFuzzerHooks", + srcs = ["src/test/java/com/example/CoverageWithHooksFuzzerHooks.java"], + create_executable = False, + deploy_manifest_lines = ["Jazzer-Hook-Classes: com.example.CoverageWithHooksFuzzerHooks"], + deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], +) + +# Regression test for https://github.com/CodeIntelligence/jazzer/issues/878: +# Custom hooks must be disabled during coverage report generation to prevent +# NoClassDefFoundError when hooked classes are used and the hook class is no +# longer available. +java_fuzz_target_test( + name = "CoverageWithHooksFuzzer", + srcs = ["src/test/java/com/example/CoverageWithHooksFuzzer.java"], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], + fuzzer_args = [ + "--coverage_report=coverage.txt", + "--instrumentation_includes=com.example.**", + ], + hook_jar = "CoverageWithHooksFuzzerHooks_deploy.jar", + target_class = "com.example.CoverageWithHooksFuzzer", + verify_crash_input = False, + verify_crash_reproducer = False, +) + java_library( name = "autofuzz_inner_class_target", srcs = ["src/test/java/com/example/AutofuzzInnerClassTarget.java"], diff --git a/tests/src/test/java/com/example/CoverageWithHooksFuzzer.java b/tests/src/test/java/com/example/CoverageWithHooksFuzzer.java new file mode 100644 index 000000000..7fd107dc8 --- /dev/null +++ b/tests/src/test/java/com/example/CoverageWithHooksFuzzer.java @@ -0,0 +1,62 @@ +/* + * 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; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import java.util.ArrayList; + +/** + * Regression test for https://github.com/CodeIntelligence/jazzer/issues/878. + * + *

When generating a coverage report at shutdown, any use of hooked method would trigger custom + * hook dispatch. If the hook class is no longer loadable at that point, the JVM throws + * NoClassDefFoundError. + * + *

This test verifies that hooks are disabled during coverage report generation by checking + * whether the hook's system property marker was set after the last fuzzer iteration. The shutdown + * sequence calls coverage report generation BEFORE fuzzerTearDown, so if hooks fire during report + * generation, the property will be set when fuzzerTearDown runs. + */ +public class CoverageWithHooksFuzzer { + public static void fuzzerTestOneInput(byte[] data) { + // Use ArrayList so the hook fires during fuzzing. + ArrayList list = new ArrayList<>(); + for (byte b : data) { + list.add(b); + } + // Verify the hook actually fired during this iteration. + if (!"true".equals(System.getProperty("jazzer.test.hook.called"))) { + throw new IllegalStateException("Hook did not fire during fuzzing"); + } + // Clear the property after all ArrayList usage in this iteration. + // If hooks fire during coverage report generation (after the last iteration), + // the property will be set again. + System.clearProperty("jazzer.test.hook.called"); + if (list.size() > 3) { + throw new FuzzerSecurityIssueLow("found enough bytes"); + } + } + + public static void fuzzerTearDown() { + // fuzzerTearDown is called AFTER coverage report generation in the shutdown sequence. + // If hooks were active during coverage report generation, use of hooked classes + // would have triggered our hook, setting the property. + if ("true".equals(System.getProperty("jazzer.test.hook.called"))) { + throw new IllegalStateException("Hook was called during coverage report generation"); + } + } +} diff --git a/tests/src/test/java/com/example/CoverageWithHooksFuzzerHooks.java b/tests/src/test/java/com/example/CoverageWithHooksFuzzerHooks.java new file mode 100644 index 000000000..30bfe4e7f --- /dev/null +++ b/tests/src/test/java/com/example/CoverageWithHooksFuzzerHooks.java @@ -0,0 +1,36 @@ +/* + * 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; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; + +/** + * Hook that targets ArrayList.<init> that sets a system property so that we can check in the + * fuzz test whether the hook is called or not. + */ +public class CoverageWithHooksFuzzerHooks { + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.util.ArrayList", + targetMethod = "") + public static void arrayListInitHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId, Object returnValue) { + System.setProperty("jazzer.test.hook.called", "true"); + } +}