• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Code Intelligence GmbH
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.code_intelligence.jazzer.driver;
18 
19 import com.code_intelligence.jazzer.agent.AgentInstaller;
20 import com.code_intelligence.jazzer.api.Jazzer;
21 import com.code_intelligence.jazzer.runtime.CoverageMap;
22 import com.code_intelligence.jazzer.utils.UnsafeProvider;
23 import java.io.ByteArrayOutputStream;
24 import java.io.PrintStream;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32 import sun.misc.Unsafe;
33 
34 public class FuzzTargetRunnerTest {
35   private static final Pattern DEDUP_TOKEN_PATTERN =
36       Pattern.compile("(?m)^DEDUP_TOKEN: ([0-9a-f]{16})(?:\r\n|\r|\n)");
37   private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
38   private static final ByteArrayOutputStream recordedErr = new ByteArrayOutputStream();
39   private static final ByteArrayOutputStream recordedOut = new ByteArrayOutputStream();
40   private static boolean fuzzerInitializeRan = false;
41   private static boolean finishedAllNonCrashingRuns = false;
42 
fuzzerInitialize()43   public static void fuzzerInitialize() {
44     fuzzerInitializeRan = true;
45   }
46 
fuzzerTestOneInput(byte[] data)47   public static void fuzzerTestOneInput(byte[] data) {
48     switch (new String(data, StandardCharsets.UTF_8)) {
49       case "no crash":
50         CoverageMap.recordCoverage(0);
51         return;
52       case "first finding":
53         CoverageMap.recordCoverage(1);
54         throw new IllegalArgumentException("first finding");
55       case "second finding":
56         CoverageMap.recordCoverage(2);
57         Jazzer.reportFindingFromHook(new StackOverflowError("second finding"));
58         throw new IllegalArgumentException("not reported");
59       case "crash":
60         CoverageMap.recordCoverage(3);
61         throw new RuntimeException("crash");
62     }
63   }
64 
fuzzerTearDown()65   public static void fuzzerTearDown() {
66     try {
67       String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8);
68       assert errOutput.contains("== Java Exception: java.lang.RuntimeException: crash");
69       String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8);
70       assert DEDUP_TOKEN_PATTERN.matcher(outOutput).find();
71 
72       assert finishedAllNonCrashingRuns : "Did not finish all expected runs before crashing";
73       assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1, 2, 3).collect(Collectors.toSet()));
74       assert UNSAFE.getByte(CoverageMap.countersAddress) == 2;
75       assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 2;
76       assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 2;
77       assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 1;
78     } catch (AssertionError e) {
79       e.printStackTrace();
80       Runtime.getRuntime().halt(1);
81     }
82     // FuzzTargetRunner calls _Exit after this function, so the test would fail unless this line is
83     // executed. Use halt rather than exit to get around FuzzTargetRunner's shutdown hook calling
84     // fuzzerTearDown, which would otherwise result in a shutdown hook loop.
85     Runtime.getRuntime().halt(0);
86   }
87 
main(String[] args)88   public static void main(String[] args) {
89     PrintStream recordingErr = new TeeOutputStream(new PrintStream(recordedErr, true), System.err);
90     System.setErr(recordingErr);
91     PrintStream recordingOut = new TeeOutputStream(new PrintStream(recordedOut, true), System.out);
92     System.setOut(recordingOut);
93 
94     // Do not instrument any classes.
95     System.setProperty("jazzer.instrumentation_excludes", "**");
96     System.setProperty("jazzer.custom_hook_excludes", "**");
97     System.setProperty("jazzer.target_class", FuzzTargetRunnerTest.class.getName());
98     // Keep going past all "no crash", "first finding" and "second finding" runs, then crash.
99     System.setProperty("jazzer.keep_going", "3");
100 
101     AgentInstaller.install(true);
102     FuzzTargetHolder.fuzzTarget =
103         FuzzTargetFinder.findFuzzTarget(FuzzTargetRunnerTest.class.getName());
104 
105     // Use a loop to simulate two findings with the same stack trace and thus verify that keep_going
106     // works as advertised.
107     for (int i = 1; i < 3; i++) {
108       int result = FuzzTargetRunner.runOne("no crash".getBytes(StandardCharsets.UTF_8));
109       if (i == 1) {
110         // Initializing FuzzTargetRunner, which happens implicitly on the first call to runOne,
111         // starts the Jazzer agent, which prints out some info messages to stdout. Ignore them.
112         recordedOut.reset();
113       }
114 
115       assert result == 0;
116       assert fuzzerInitializeRan;
117       assert CoverageMap.getCoveredIds().equals(Stream.of(0).collect(Collectors.toSet()));
118       assert UNSAFE.getByte(CoverageMap.countersAddress) == i;
119       assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 0;
120       assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 0;
121       assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0;
122 
123       String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8);
124       List<String> unexpectedLines = Arrays.stream(errOutput.split("\n"))
125                                          .filter(line -> !line.startsWith("INFO: "))
126                                          .collect(Collectors.toList());
127       assert unexpectedLines.isEmpty()
128           : "Unexpected output on System.err: '"
129           + String.join("\n", unexpectedLines) + "'";
130       String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8);
131       assert outOutput.isEmpty() : "Non-empty System.out: '" + outOutput + "'";
132     }
133 
134     String firstDedupToken = null;
135     for (int i = 1; i < 3; i++) {
136       int result = FuzzTargetRunner.runOne("first finding".getBytes(StandardCharsets.UTF_8));
137 
138       assert result == 0;
139       assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1).collect(Collectors.toSet()));
140       assert UNSAFE.getByte(CoverageMap.countersAddress) == 2;
141       assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == i;
142       assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == 0;
143       assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0;
144 
145       String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8);
146       String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8);
147       if (i == 1) {
148         assert errOutput.contains(
149             "== Java Exception: java.lang.IllegalArgumentException: first finding");
150         Matcher dedupTokenMatcher = DEDUP_TOKEN_PATTERN.matcher(outOutput);
151         assert dedupTokenMatcher.matches() : "Unexpected output on System.out: '" + outOutput + "'";
152         firstDedupToken = dedupTokenMatcher.group();
153         recordedErr.reset();
154         recordedOut.reset();
155       } else {
156         assert errOutput.isEmpty();
157         assert outOutput.isEmpty();
158       }
159     }
160 
161     for (int i = 1; i < 3; i++) {
162       int result = FuzzTargetRunner.runOne("second finding".getBytes(StandardCharsets.UTF_8));
163 
164       assert result == 0;
165       assert CoverageMap.getCoveredIds().equals(Stream.of(0, 1, 2).collect(Collectors.toSet()));
166       assert UNSAFE.getByte(CoverageMap.countersAddress) == 2;
167       assert UNSAFE.getByte(CoverageMap.countersAddress + 1) == 2;
168       assert UNSAFE.getByte(CoverageMap.countersAddress + 2) == i;
169       assert UNSAFE.getByte(CoverageMap.countersAddress + 3) == 0;
170 
171       String errOutput = new String(recordedErr.toByteArray(), StandardCharsets.UTF_8);
172       String outOutput = new String(recordedOut.toByteArray(), StandardCharsets.UTF_8);
173       if (i == 1) {
174         // Verify that the StackOverflowError is wrapped in security issue and contains reproducer
175         // information.
176         assert errOutput.contains(
177             "== Java Exception: com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow: Stack overflow (use ");
178         assert !errOutput.contains("not reported");
179         Matcher dedupTokenMatcher = DEDUP_TOKEN_PATTERN.matcher(outOutput);
180         assert dedupTokenMatcher.matches() : "Unexpected output on System.out: '" + outOutput + "'";
181         assert !firstDedupToken.equals(dedupTokenMatcher.group());
182         recordedErr.reset();
183         recordedOut.reset();
184       } else {
185         assert errOutput.isEmpty();
186         assert outOutput.isEmpty();
187       }
188     }
189 
190     finishedAllNonCrashingRuns = true;
191 
192     FuzzTargetRunner.runOne("crash".getBytes(StandardCharsets.UTF_8));
193 
194     throw new IllegalStateException("Expected FuzzTargetRunner to call fuzzerTearDown");
195   }
196 
197   /**
198    * An OutputStream that prints to two OutputStreams simultaneously.
199    */
200   private static class TeeOutputStream extends PrintStream {
201     private final PrintStream otherOut;
TeeOutputStream(PrintStream out1, PrintStream out2)202     public TeeOutputStream(PrintStream out1, PrintStream out2) {
203       super(out1, true);
204       this.otherOut = out2;
205     }
206 
207     @Override
flush()208     public void flush() {
209       super.flush();
210       otherOut.flush();
211     }
212 
213     @Override
close()214     public void close() {
215       super.close();
216       otherOut.close();
217     }
218 
219     @Override
write(int b)220     public void write(int b) {
221       super.write(b);
222       otherOut.write(b);
223     }
224 
225     @Override
write(byte[] buf, int off, int len)226     public void write(byte[] buf, int off, int len) {
227       super.write(buf, off, len);
228       otherOut.write(buf, off, len);
229     }
230   }
231 }
232