1 /* 2 * Copyright (C) 2019 The Android Open Source Project 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.android.compatibility.common.util; 18 19 import java.io.File; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.Collection; 23 import java.util.Collections; 24 import java.util.List; 25 import java.util.regex.Matcher; 26 import java.util.regex.Pattern; 27 import java.util.stream.Collectors; 28 import java.util.stream.IntStream; 29 import java.util.stream.Stream; 30 import java.math.BigInteger; 31 import org.json.JSONArray; 32 import org.json.JSONException; 33 import org.json.JSONObject; 34 35 /** Contains helper functions and shared constants for crash parsing. */ 36 public class CrashUtils { 37 // used to only detect actual addresses instead of nullptr and other unlikely values 38 public static final BigInteger MIN_CRASH_ADDR = new BigInteger("8000", 16); 39 // Matches the end of a crash 40 public static final Pattern sEndofCrashPattern = 41 Pattern.compile("DEBUG\\s+?:\\s+?backtrace:"); 42 public static final String DEVICE_PATH = "/data/local/tmp/CrashParserResults/"; 43 public static final String LOCK_FILENAME = "lockFile.loc"; 44 public static final String UPLOAD_REQUEST = "Please upload a result file to stagefright"; 45 public static final Pattern sUploadRequestPattern = 46 Pattern.compile(UPLOAD_REQUEST); 47 public static final String NEW_TEST_ALERT = "New test starting with name: "; 48 public static final Pattern sNewTestPattern = 49 Pattern.compile(NEW_TEST_ALERT + "(\\w+?)\\(.*?\\)"); 50 public static final String SIGNAL = "signal"; 51 public static final String ABORT_MESSAGE = "abortmessage"; 52 public static final String NAME = "name"; 53 public static final String PROCESS = "process"; 54 public static final String PID = "pid"; 55 public static final String TID = "tid"; 56 public static final String FAULT_ADDRESS = "faultaddress"; 57 public static final String FILENAME = "filename"; 58 public static final String METHOD = "method"; 59 public static final String BACKTRACE = "backtrace"; 60 // Matches the smallest blob that has the appropriate header and footer 61 private static final Pattern sCrashBlobPattern = 62 Pattern.compile("DEBUG\\s+?:( [*]{3})+?.*?DEBUG\\s+?:\\s+?backtrace:", Pattern.DOTALL); 63 // Matches process id and name line and captures them 64 private static final Pattern sPidtidNamePattern = 65 Pattern.compile("pid: (\\d+?), tid: (\\d+?), name: ([^\\s]+?\\s+?)*?>>> (.*?) <<<"); 66 // Matches fault address and signal type line 67 private static final Pattern sFaultLinePattern = 68 Pattern.compile( 69 "\\w+? \\d+? \\((.*?)\\), code -*?\\d+? \\(.*?\\), fault addr " 70 + "(?:0x(\\p{XDigit}+)|-+)"); 71 // Matches the abort message line 72 private static Pattern sAbortMessagePattern = Pattern.compile("(?i)Abort message: (.*)"); 73 // Matches one backtrace NOTE line, exactly as tombstone_proto_to_text's print_thread_backtrace 74 private static Pattern sBacktraceNotePattern = 75 Pattern.compile("[0-9\\-\\s:.]+[A-Z] DEBUG\\s+:\\s+NOTE: .*"); 76 // Matches one backtrace frame, exactly as tombstone_proto_to_text's print_backtrace 77 // Two versions, because we want to exclude the BuildID section if it exists 78 private static Pattern sBacktraceFrameWithBuildIdPattern = 79 Pattern.compile( 80 "[0-9\\-\\s:.]+[A-Z] DEBUG\\s+:\\s+#[0-9]+ pc [0-9a-fA-F]+ " 81 + "(?<filename>[^\\s]+)(\\s+\\((?<method>.*)\\))?" 82 + "\\s+\\(BuildId: .*\\)"); 83 private static Pattern sBacktraceFrameWithoutBuildIdPattern = 84 Pattern.compile( 85 "[0-9\\-\\s:.]+[A-Z] DEBUG\\s+:\\s+#[0-9]+ pc [0-9a-fA-F]+ " 86 + "(?<filename>[^\\s]+)(\\s+\\((?<method>.*)\\))?"); 87 88 public static final String SIGSEGV = "SIGSEGV"; 89 public static final String SIGBUS = "SIGBUS"; 90 public static final String SIGABRT = "SIGABRT"; 91 92 /** 93 * returns the filename of the process. 94 * e.g. "/system/bin/mediaserver" returns "mediaserver" 95 */ getProcessFileName(JSONObject crash)96 public static String getProcessFileName(JSONObject crash) throws JSONException { 97 return new File(crash.getString(PROCESS)).getName(); 98 } 99 100 /** 101 * Determines if the given input has a {@link com.android.compatibility.common.util.Crash} that 102 * should fail an sts test 103 * 104 * @param crashes list of crashes to check 105 * @param config crash detection configuration object 106 * @return if a crash is serious enough to fail an sts test 107 */ securityCrashDetected(JSONArray crashes, Config config)108 public static boolean securityCrashDetected(JSONArray crashes, Config config) { 109 return matchSecurityCrashes(crashes, config).length() > 0; 110 } 111 getBigInteger(JSONObject source, String name)112 public static BigInteger getBigInteger(JSONObject source, String name) throws JSONException { 113 if (source.isNull(name)) { 114 return null; 115 } 116 String intString = source.getString(name); 117 BigInteger value = null; 118 try { 119 value = new BigInteger(intString, 16); 120 } catch (NumberFormatException e) {} 121 return value; 122 } 123 124 /** 125 * Determines which given inputs have a {@link com.android.compatibility.common.util.Crash} that 126 * should fail an sts test 127 * 128 * @param crashes list of crashes to check 129 * @param config crash detection configuration object 130 * @return the list of crashes serious enough to fail an sts test 131 */ matchSecurityCrashes(JSONArray crashes, Config config)132 public static JSONArray matchSecurityCrashes(JSONArray crashes, Config config) { 133 JSONArray securityCrashes = new JSONArray(); 134 for (int i = 0; i < crashes.length(); i++) { 135 try { 136 JSONObject crash = crashes.getJSONObject(i); 137 138 // match process patterns 139 if (!matchesAny(getProcessFileName(crash), config.processPatterns)) { 140 continue; 141 } 142 143 // match signal 144 String crashSignal = crash.getString(SIGNAL); 145 if (!config.signals.contains(crashSignal)) { 146 continue; 147 } 148 149 if (crash.has(ABORT_MESSAGE)) { 150 String crashAbortMessage = crash.getString(ABORT_MESSAGE); 151 if (!config.abortMessageIncludes.isEmpty()) { 152 if (!config.abortMessageIncludes.stream() 153 .filter(p -> p.matcher(crashAbortMessage).find()) 154 .findFirst() 155 .isPresent()) { 156 continue; 157 } 158 } 159 if (config.abortMessageExcludes.stream() 160 .filter(p -> p.matcher(crashAbortMessage).find()) 161 .findFirst() 162 .isPresent()) { 163 continue; 164 } 165 } 166 167 // if check specified, reject crash if address is unlikely to be security-related 168 if (config.checkMinAddress) { 169 BigInteger faultAddress = getBigInteger(crash, FAULT_ADDRESS); 170 if (faultAddress != null 171 && faultAddress.compareTo(config.minCrashAddress) < 0) { 172 continue; 173 } 174 } 175 176 JSONArray backtrace = crash.getJSONArray(BACKTRACE); 177 178 /* if backtrace "includes" patterns are present, ignore this crash if there is no 179 * frame that matches any of the patterns 180 */ 181 List<Config.BacktraceFilterPattern> backtraceIncludes = 182 config.getBacktraceIncludes(); 183 if (!backtraceIncludes.isEmpty()) { 184 if (!IntStream.range(0, backtrace.length()) 185 .mapToObj(j -> backtrace.optJSONObject(j)) 186 .flatMap(frame -> backtraceIncludes.stream().map(p -> p.match(frame))) 187 .anyMatch(matched -> matched)) { 188 continue; 189 } 190 } 191 192 /* if backtrace "excludes" patterns are present, ignore this crash if there is any 193 * frame that matches any of the patterns 194 */ 195 List<Config.BacktraceFilterPattern> backtraceExcludes = 196 config.getBacktraceExcludes(); 197 if (!backtraceExcludes.isEmpty()) { 198 if (IntStream.range(0, backtrace.length()) 199 .mapToObj(j -> backtrace.optJSONObject(j)) 200 .flatMap(frame -> backtraceExcludes.stream().map(p -> p.match(frame))) 201 .anyMatch(matched -> matched)) { 202 continue; 203 } 204 } 205 206 securityCrashes.put(crash); 207 } catch (JSONException | NullPointerException e) {} 208 } 209 return securityCrashes; 210 } 211 212 /** 213 * returns true if the input matches any of the patterns. 214 */ matchesAny(String input, Collection<Pattern> patterns)215 private static boolean matchesAny(String input, Collection<Pattern> patterns) { 216 for (Pattern p : patterns) { 217 if (p.matcher(input).matches()) { 218 return true; 219 } 220 } 221 return false; 222 } 223 224 /** Adds all crashes found in the input as JSONObjects to the given JSONArray */ addAllCrashes(String input, JSONArray crashes)225 public static JSONArray addAllCrashes(String input, JSONArray crashes) { 226 Matcher crashBlobFinder = sCrashBlobPattern.matcher(input); 227 while (crashBlobFinder.find()) { 228 String crashStr = crashBlobFinder.group(0); 229 int tid = 0; 230 int pid = 0; 231 BigInteger faultAddress = null; 232 String name = null; 233 String process = null; 234 String signal = null; 235 String abortMessage = null; 236 List<BacktraceFrameInfo> backtraceFrames = new ArrayList<BacktraceFrameInfo>(); 237 238 Matcher pidtidNameMatcher = sPidtidNamePattern.matcher(crashStr); 239 if (pidtidNameMatcher.find()) { 240 try { 241 pid = Integer.parseInt(pidtidNameMatcher.group(1)); 242 } catch (NumberFormatException e) { 243 } 244 try { 245 tid = Integer.parseInt(pidtidNameMatcher.group(2)); 246 } catch (NumberFormatException e) { 247 } 248 name = pidtidNameMatcher.group(3).trim(); 249 process = pidtidNameMatcher.group(4).trim(); 250 } 251 252 Matcher faultLineMatcher = sFaultLinePattern.matcher(crashStr); 253 if (faultLineMatcher.find()) { 254 signal = faultLineMatcher.group(1); 255 String faultAddrMatch = faultLineMatcher.group(2); 256 if (faultAddrMatch != null) { 257 try { 258 faultAddress = new BigInteger(faultAddrMatch, 16); 259 } catch (NumberFormatException e) { 260 } 261 } 262 } 263 264 Matcher abortMessageMatcher = sAbortMessagePattern.matcher(crashStr); 265 if (abortMessageMatcher.find()) { 266 abortMessage = abortMessageMatcher.group(1); 267 } 268 269 // Continue on after the crash block to find all the stacktrace entries. 270 // The format is from tombstone_proto_to_text.cpp's print_thread_backtrace() 271 // This will scan the logcat lines until it finds a line that does not match, 272 // or end of log. 273 int currentIndex = crashBlobFinder.end(); 274 while (true) { 275 int firstEndline = input.indexOf('\n', currentIndex); 276 int secondEndline = input.indexOf('\n', firstEndline + 1); 277 currentIndex = secondEndline; 278 if (firstEndline == -1 || secondEndline == -1) break; 279 280 String nextLine = input.substring(firstEndline + 1, secondEndline); 281 282 Matcher backtraceNoteMatcher = sBacktraceNotePattern.matcher(nextLine); 283 if (backtraceNoteMatcher.matches()) { 284 continue; 285 } 286 287 Matcher backtraceFrameWithBuildIdMatcher = 288 sBacktraceFrameWithBuildIdPattern.matcher(nextLine); 289 Matcher backtraceFrameWithoutBuildIdMatcher = 290 sBacktraceFrameWithoutBuildIdPattern.matcher(nextLine); 291 292 Matcher backtraceFrameMatcher = null; 293 if (backtraceFrameWithBuildIdMatcher.matches()) { 294 backtraceFrameMatcher = backtraceFrameWithBuildIdMatcher; 295 296 } else if (backtraceFrameWithoutBuildIdMatcher.matches()) { 297 backtraceFrameMatcher = backtraceFrameWithoutBuildIdMatcher; 298 299 } else { 300 break; 301 } 302 303 backtraceFrames.add( 304 new BacktraceFrameInfo( 305 backtraceFrameMatcher.group("filename"), 306 backtraceFrameMatcher.group("method"))); 307 } 308 309 try { 310 JSONObject crash = new JSONObject(); 311 crash.put(PID, pid); 312 crash.put(TID, tid); 313 crash.put(NAME, name); 314 crash.put(PROCESS, process); 315 crash.put(FAULT_ADDRESS, faultAddress == null ? null : faultAddress.toString(16)); 316 crash.put(SIGNAL, signal); 317 crash.put(ABORT_MESSAGE, abortMessage); 318 JSONArray backtrace = new JSONArray(); 319 for (BacktraceFrameInfo frame : backtraceFrames) { 320 backtrace.put( 321 new JSONObject() 322 .put(FILENAME, frame.getFilename()) 323 .put(METHOD, frame.getMethod())); 324 } 325 crash.put(BACKTRACE, backtrace); 326 crashes.put(crash); 327 } catch (JSONException e) {} 328 } 329 return crashes; 330 } 331 332 public static class BacktraceFrameInfo { 333 private final String filename; 334 private final String method; 335 BacktraceFrameInfo(String filename, String method)336 public BacktraceFrameInfo(String filename, String method) { 337 this.filename = filename; 338 this.method = method; 339 } 340 getFilename()341 public String getFilename() { 342 return this.filename; 343 } 344 getMethod()345 public String getMethod() { 346 return this.method; 347 } 348 } 349 350 public static class Config { 351 private boolean checkMinAddress; 352 private BigInteger minCrashAddress; 353 private List<String> signals; 354 private List<Pattern> processPatterns; 355 private List<Pattern> abortMessageIncludes; 356 private List<Pattern> abortMessageExcludes; 357 private List<BacktraceFilterPattern> backtraceIncludes; 358 private List<BacktraceFilterPattern> backtraceExcludes; 359 Config()360 public Config() { 361 checkMinAddress = true; 362 minCrashAddress = MIN_CRASH_ADDR; 363 setSignals(SIGSEGV, SIGBUS); 364 abortMessageIncludes = new ArrayList<>(); 365 setAbortMessageExcludes("CHECK_", "CANNOT LINK EXECUTABLE"); 366 processPatterns = new ArrayList(); 367 backtraceIncludes = new ArrayList(); 368 backtraceExcludes = new ArrayList(); 369 } 370 setMinAddress(BigInteger minCrashAddress)371 public Config setMinAddress(BigInteger minCrashAddress) { 372 this.minCrashAddress = minCrashAddress; 373 return this; 374 } 375 checkMinAddress(boolean checkMinAddress)376 public Config checkMinAddress(boolean checkMinAddress) { 377 this.checkMinAddress = checkMinAddress; 378 return this; 379 } 380 setSignals(String... signals)381 public Config setSignals(String... signals) { 382 this.signals = new ArrayList(Arrays.asList(signals)); 383 return this; 384 } 385 appendSignals(String... signals)386 public Config appendSignals(String... signals) { 387 Collections.addAll(this.signals, signals); 388 return this; 389 } 390 setAbortMessageIncludes(String... abortMessages)391 public Config setAbortMessageIncludes(String... abortMessages) { 392 this.abortMessageIncludes = new ArrayList<>(toPatterns(abortMessages)); 393 return this; 394 } 395 setAbortMessageIncludes(Pattern... abortMessages)396 public Config setAbortMessageIncludes(Pattern... abortMessages) { 397 this.abortMessageIncludes = new ArrayList<>(Arrays.asList(abortMessages)); 398 return this; 399 } 400 appendAbortMessageIncludes(String... abortMessages)401 public Config appendAbortMessageIncludes(String... abortMessages) { 402 this.abortMessageIncludes.addAll(toPatterns(abortMessages)); 403 return this; 404 } 405 appendAbortMessageIncludes(Pattern... abortMessages)406 public Config appendAbortMessageIncludes(Pattern... abortMessages) { 407 Collections.addAll(this.abortMessageIncludes, abortMessages); 408 return this; 409 } 410 setAbortMessageExcludes(String... abortMessages)411 public Config setAbortMessageExcludes(String... abortMessages) { 412 this.abortMessageExcludes = new ArrayList<>(toPatterns(abortMessages)); 413 return this; 414 } 415 setAbortMessageExcludes(Pattern... abortMessages)416 public Config setAbortMessageExcludes(Pattern... abortMessages) { 417 this.abortMessageExcludes = new ArrayList<>(Arrays.asList(abortMessages)); 418 return this; 419 } 420 appendAbortMessageExcludes(String... abortMessages)421 public Config appendAbortMessageExcludes(String... abortMessages) { 422 this.abortMessageExcludes.addAll(toPatterns(abortMessages)); 423 return this; 424 } 425 appendAbortMessageExcludes(Pattern... abortMessages)426 public Config appendAbortMessageExcludes(Pattern... abortMessages) { 427 Collections.addAll(this.abortMessageExcludes, abortMessages); 428 return this; 429 } 430 431 setProcessPatterns(String... processPatternStrings)432 public Config setProcessPatterns(String... processPatternStrings) { 433 this.processPatterns = new ArrayList<>(toPatterns(processPatternStrings)); 434 return this; 435 } 436 setProcessPatterns(Pattern... processPatterns)437 public Config setProcessPatterns(Pattern... processPatterns) { 438 this.processPatterns = new ArrayList(Arrays.asList(processPatterns)); 439 return this; 440 } 441 getProcessPatterns()442 public List<Pattern> getProcessPatterns() { 443 return Collections.unmodifiableList(processPatterns); 444 } 445 appendProcessPatterns(String... processPatternStrings)446 public Config appendProcessPatterns(String... processPatternStrings) { 447 this.processPatterns.addAll(toPatterns(processPatternStrings)); 448 return this; 449 } 450 appendProcessPatterns(Pattern... processPatterns)451 public Config appendProcessPatterns(Pattern... processPatterns) { 452 Collections.addAll(this.processPatterns, processPatterns); 453 return this; 454 } 455 setBacktraceIncludes(BacktraceFilterPattern... patterns)456 public Config setBacktraceIncludes(BacktraceFilterPattern... patterns) { 457 this.backtraceIncludes = new ArrayList<>(Arrays.asList(patterns)); 458 return this; 459 } 460 getBacktraceIncludes()461 public List<BacktraceFilterPattern> getBacktraceIncludes() { 462 return Collections.unmodifiableList(this.backtraceIncludes); 463 } 464 appendBacktraceIncludes(BacktraceFilterPattern... patterns)465 public Config appendBacktraceIncludes(BacktraceFilterPattern... patterns) { 466 Collections.addAll(this.backtraceIncludes, patterns); 467 return this; 468 } 469 setBacktraceExcludes(BacktraceFilterPattern... patterns)470 public Config setBacktraceExcludes(BacktraceFilterPattern... patterns) { 471 this.backtraceExcludes = new ArrayList<>(Arrays.asList(patterns)); 472 return this; 473 } 474 getBacktraceExcludes()475 public List<BacktraceFilterPattern> getBacktraceExcludes() { 476 return Collections.unmodifiableList(this.backtraceExcludes); 477 } 478 appendBacktraceExcludes(BacktraceFilterPattern... patterns)479 public Config appendBacktraceExcludes(BacktraceFilterPattern... patterns) { 480 Collections.addAll(this.backtraceExcludes, patterns); 481 return this; 482 } 483 484 /** 485 * A utility class that contains patterns to filter backtraces on. 486 * 487 * <p>A filter matches if any of the backtrace frame matches any of the patterns. 488 * 489 * <p>Either filenamePattern or methodPattern can be null, in which case it will act like a 490 * wildcard pattern and matches anything. 491 * 492 * <p>A null filename or method name will not match any non-null pattern. 493 */ 494 public static class BacktraceFilterPattern { 495 private final Pattern filenamePattern; 496 private final Pattern methodPattern; 497 498 /** 499 * Constructs a BacktraceFilterPattern with the given file and method name patterns. 500 * 501 * <p>Null patterns are interpreted as wildcards and match anything. 502 * 503 * @param filenamePattern Regex string for the filename pattern. Can be null. 504 * @param methodPattern Regex string for the method name pattern. Can be null. 505 */ BacktraceFilterPattern(String filenamePattern, String methodPattern)506 public BacktraceFilterPattern(String filenamePattern, String methodPattern) { 507 if (filenamePattern == null) { 508 this.filenamePattern = null; 509 } else { 510 this.filenamePattern = Pattern.compile(filenamePattern); 511 } 512 513 if (methodPattern == null) { 514 this.methodPattern = null; 515 } else { 516 this.methodPattern = Pattern.compile(methodPattern); 517 } 518 } 519 520 /** Returns true if the current patterns match a backtrace frame. */ match(JSONObject frame)521 public boolean match(JSONObject frame) { 522 if (frame == null) return false; 523 524 String filename = frame.optString(FILENAME); 525 String method = frame.optString(METHOD); 526 527 boolean filenameMatches = 528 (filenamePattern == null 529 || (filename != null && filenamePattern.matcher(filename).find())); 530 boolean methodMatches = 531 (methodPattern == null 532 || (method != null && methodPattern.matcher(method).find())); 533 return filenameMatches && methodMatches; 534 } 535 } 536 } 537 toPatterns(String... patternStrings)538 private static List<Pattern> toPatterns(String... patternStrings) { 539 return Stream.of(patternStrings).map(Pattern::compile).collect(Collectors.toList()); 540 } 541 } 542