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