• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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