1 // Copyright 2022 Code Intelligence GmbH 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.code_intelligence.jazzer.junit; 16 17 import java.io.IOException; 18 import java.lang.reflect.Field; 19 import java.lang.reflect.Method; 20 import java.util.List; 21 import java.util.Optional; 22 import java.util.concurrent.atomic.AtomicReference; 23 import org.junit.jupiter.api.extension.ConditionEvaluationResult; 24 import org.junit.jupiter.api.extension.ExecutionCondition; 25 import org.junit.jupiter.api.extension.ExtensionContext; 26 import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 27 import org.junit.jupiter.api.extension.InvocationInterceptor; 28 import org.junit.jupiter.api.extension.ReflectiveInvocationContext; 29 import org.junit.platform.commons.support.AnnotationSupport; 30 31 class FuzzTestExtensions implements ExecutionCondition, InvocationInterceptor { 32 private static final String JAZZER_INTERNAL = 33 "com.code_intelligence.jazzer.runtime.JazzerInternal"; 34 private static final AtomicReference<Method> fuzzTestMethod = new AtomicReference<>(); 35 private static Field lastFindingField; 36 private static Field hooksEnabledField; 37 38 @Override interceptTestTemplateMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext)39 public void interceptTestTemplateMethod(Invocation<Void> invocation, 40 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 41 throws Throwable { 42 FuzzTest fuzzTest = 43 AnnotationSupport.findAnnotation(invocationContext.getExecutable(), FuzzTest.class).get(); 44 FuzzTestExecutor.configureAndInstallAgent(extensionContext, fuzzTest.maxDuration()); 45 // Skip the invocation of the test method with the special arguments provided by 46 // FuzzTestArgumentsProvider and start fuzzing instead. 47 if (Utils.isMarkedInvocation(invocationContext)) { 48 startFuzzing(invocation, invocationContext, extensionContext); 49 } else { 50 // Blocked by https://github.com/junit-team/junit5/issues/3282: 51 // TODO: The seeds from the input directory are duplicated here as there is no way to 52 // recognize them. 53 // TODO: Error out if there is a non-Jazzer ArgumentsProvider and the SeedSerializer does not 54 // support write. 55 if (Utils.isFuzzing(extensionContext)) { 56 // JUnit verifies that the arguments for this invocation are valid. 57 recordSeedForFuzzing(invocationContext.getArguments(), extensionContext); 58 } 59 runWithHooks(invocation); 60 } 61 } 62 63 /** 64 * Mimics the logic of Jazzer's FuzzTargetRunner, which reports findings in the following way: 65 * <ol> 66 * <li>If a hook used Jazzer#reportFindingFromHook to explicitly report a finding, the last such 67 * finding, as stored in JazzerInternal#lastFinding, is reported. <li>If the fuzz target method 68 * threw a Throwable, that is reported. <li>3. Otherwise, nothing is reported. 69 * </ol> 70 */ runWithHooks(Invocation<Void> invocation)71 private static void runWithHooks(Invocation<Void> invocation) throws Throwable { 72 Throwable thrown = null; 73 getLastFindingField().set(null, null); 74 // When running in regression test mode, the agent emits additional bytecode logic in front of 75 // method hook invocations that enables them only while a global variable managed by 76 // withHooksEnabled is true. 77 // 78 // Alternatives considered: 79 // * Using a dedicated class loader for @FuzzTests: First-class support for this isn't 80 // available in JUnit 5 (https://github.com/junit-team/junit5/issues/201), but 81 // third-party extensions have done it: 82 // https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java 83 // However, as this involves launching a new test run as part of running a test, this 84 // introduces a number of inconsistencies if applied on the test method rather than test 85 // class level. For example, @BeforeAll methods will have to be run twice in different class 86 // loaders, which may not be safe if they are using global resources not separated by class 87 // loaders (e.g. files). 88 try (AutoCloseable ignored = withHooksEnabled()) { 89 invocation.proceed(); 90 } catch (Throwable t) { 91 thrown = t; 92 } 93 Throwable stored = (Throwable) getLastFindingField().get(null); 94 if (stored != null) { 95 throw stored; 96 } else if (thrown != null) { 97 throw thrown; 98 } 99 } 100 startFuzzing(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext)101 private static void startFuzzing(Invocation<Void> invocation, 102 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 103 throws Throwable { 104 invocation.skip(); 105 Optional<Throwable> throwable = 106 FuzzTestExecutor.fromContext(extensionContext) 107 .execute(invocationContext, getOrCreateSeedSerializer(extensionContext)); 108 if (throwable.isPresent()) { 109 throw throwable.get(); 110 } 111 } 112 recordSeedForFuzzing(List<Object> arguments, ExtensionContext extensionContext)113 private void recordSeedForFuzzing(List<Object> arguments, ExtensionContext extensionContext) 114 throws IOException { 115 SeedSerializer seedSerializer = getOrCreateSeedSerializer(extensionContext); 116 try { 117 FuzzTestExecutor.fromContext(extensionContext) 118 .addSeed(seedSerializer.write(arguments.toArray())); 119 } catch (UnsupportedOperationException ignored) { 120 } 121 } 122 123 @Override evaluateExecutionCondition(ExtensionContext extensionContext)124 public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) { 125 if (!Utils.isFuzzing(extensionContext)) { 126 return ConditionEvaluationResult.enabled( 127 "Regression tests are run instead of fuzzing since JAZZER_FUZZ has not been set to a non-empty value"); 128 } 129 // Only fuzz the first @FuzzTest that makes it here. 130 if (FuzzTestExtensions.fuzzTestMethod.compareAndSet( 131 null, extensionContext.getRequiredTestMethod()) 132 || extensionContext.getRequiredTestMethod().equals( 133 FuzzTestExtensions.fuzzTestMethod.get())) { 134 return ConditionEvaluationResult.enabled( 135 "Fuzzing " + extensionContext.getRequiredTestMethod()); 136 } 137 return ConditionEvaluationResult.disabled( 138 "Only one fuzz test can be run at a time, but multiple tests have been annotated with @FuzzTest"); 139 } 140 getOrCreateSeedSerializer(ExtensionContext extensionContext)141 private static SeedSerializer getOrCreateSeedSerializer(ExtensionContext extensionContext) { 142 Method method = extensionContext.getRequiredTestMethod(); 143 return extensionContext.getStore(Namespace.create(FuzzTestExtensions.class, method)) 144 .getOrComputeIfAbsent( 145 SeedSerializer.class, unused -> SeedSerializer.of(method), SeedSerializer.class); 146 } 147 getLastFindingField()148 private static Field getLastFindingField() throws ClassNotFoundException, NoSuchFieldException { 149 if (lastFindingField == null) { 150 Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL); 151 lastFindingField = jazzerInternal.getField("lastFinding"); 152 } 153 return lastFindingField; 154 } 155 getHooksEnabledField()156 private static Field getHooksEnabledField() throws ClassNotFoundException, NoSuchFieldException { 157 if (hooksEnabledField == null) { 158 Class<?> jazzerInternal = Class.forName(JAZZER_INTERNAL); 159 hooksEnabledField = jazzerInternal.getField("hooksEnabled"); 160 } 161 return hooksEnabledField; 162 } 163 withHooksEnabled()164 private static AutoCloseable withHooksEnabled() 165 throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException { 166 Field hooksEnabledField = getHooksEnabledField(); 167 hooksEnabledField.setBoolean(null, true); 168 return () -> hooksEnabledField.setBoolean(null, false); 169 } 170 } 171