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