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