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