diff options
Diffstat (limited to 'testing/mozbase/mozleak/mozleak/lsan.py')
-rw-r--r-- | testing/mozbase/mozleak/mozleak/lsan.py | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/testing/mozbase/mozleak/mozleak/lsan.py b/testing/mozbase/mozleak/mozleak/lsan.py new file mode 100644 index 0000000000..f6555eff2d --- /dev/null +++ b/testing/mozbase/mozleak/mozleak/lsan.py @@ -0,0 +1,220 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import re + + +class LSANLeaks(object): + + """ + Parses the log when running an LSAN build, looking for interesting stack frames + in allocation stacks + """ + + def __init__( + self, + logger, + scope=None, + allowed=None, + maxNumRecordedFrames=None, + allowAll=False, + ): + self.logger = logger + self.inReport = False + self.fatalError = False + self.symbolizerError = False + self.foundFrames = set() + self.recordMoreFrames = None + self.currStack = None + self.maxNumRecordedFrames = maxNumRecordedFrames if maxNumRecordedFrames else 4 + self.summaryData = None + self.scope = scope + self.allowedMatch = None + self.allowAll = allowAll + self.sawError = False + + # Don't various allocation-related stack frames, as they do not help much to + # distinguish different leaks. + unescapedSkipList = [ + "malloc", + "js_malloc", + "malloc_", + "__interceptor_malloc", + "moz_xmalloc", + "calloc", + "js_calloc", + "calloc_", + "__interceptor_calloc", + "moz_xcalloc", + "realloc", + "js_realloc", + "realloc_", + "__interceptor_realloc", + "moz_xrealloc", + "new", + "js::MallocProvider", + ] + self.skipListRegExp = re.compile( + "^" + "|".join([re.escape(f) for f in unescapedSkipList]) + "$" + ) + + self.startRegExp = re.compile( + "==\d+==ERROR: LeakSanitizer: detected memory leaks" + ) + self.fatalErrorRegExp = re.compile( + "==\d+==LeakSanitizer has encountered a fatal error." + ) + self.symbolizerOomRegExp = re.compile( + "LLVMSymbolizer: error reading file: Cannot allocate memory" + ) + self.stackFrameRegExp = re.compile(" #\d+ 0x[0-9a-f]+ in ([^(</]+)") + self.sysLibStackFrameRegExp = re.compile( + " #\d+ 0x[0-9a-f]+ \(([^+]+)\+0x[0-9a-f]+\)" + ) + self.summaryRegexp = re.compile( + "SUMMARY: AddressSanitizer: (\d+) byte\(s\) leaked in (\d+) allocation\(s\)." + ) + self.rustRegexp = re.compile("::h[a-f0-9]{16}$") + self.setAllowed(allowed) + + def setAllowed(self, allowedLines): + if not allowedLines or self.allowAll: + self.allowedRegexp = None + else: + self.allowedRegexp = re.compile( + "^" + "|".join([re.escape(f) for f in allowedLines]) + ) + + def log(self, line): + if re.match(self.startRegExp, line): + self.inReport = True + # Downgrade this from an ERROR + self.sawError = True + return "LeakSanitizer: detected memory leaks" + + if re.match(self.fatalErrorRegExp, line): + self.fatalError = True + return line + + if re.match(self.symbolizerOomRegExp, line): + self.symbolizerError = True + return line + + if not self.inReport: + return line + + if line.startswith("Direct leak") or line.startswith("Indirect leak"): + self._finishStack() + self.recordMoreFrames = True + self.currStack = [] + return line + + summaryData = self.summaryRegexp.match(line) + if summaryData: + assert self.summaryData is None + self._finishStack() + self.inReport = False + self.summaryData = (int(item) for item in summaryData.groups()) + # We don't return the line here because we want to control whether the + # leak is seen as an expected failure later + return + + if not self.recordMoreFrames: + return line + + stackFrame = re.match(self.stackFrameRegExp, line) + if stackFrame: + # Split the frame to remove any return types. + frame = stackFrame.group(1).split()[-1] + if not re.match(self.skipListRegExp, frame): + self._recordFrame(frame) + return line + + sysLibStackFrame = re.match(self.sysLibStackFrameRegExp, line) + if sysLibStackFrame: + # System library stack frames will never match the skip list, + # so don't bother checking if they do. + self._recordFrame(sysLibStackFrame.group(1)) + + # If we don't match either of these, just ignore the frame. + # We'll end up with "unknown stack" if everything is ignored. + return line + + def process(self): + failures = 0 + + if self.allowAll: + self.logger.info("LeakSanitizer | Leak checks disabled") + return + + if self.summaryData: + allowed = all(allowed for _, allowed in self.foundFrames) + self.logger.lsan_summary(*self.summaryData, allowed=allowed) + self.summaryData = None + + if self.fatalError: + self.logger.error( + "LeakSanitizer | LeakSanitizer has encountered a fatal error." + ) + failures += 1 + + if self.symbolizerError: + self.logger.error( + "LeakSanitizer | LLVMSymbolizer was unable to allocate memory.\n" + "This will cause leaks that " + "should be ignored to instead be reported as an error" + ) + failures += 1 + + if self.foundFrames: + self.logger.info( + "LeakSanitizer | To show the " + "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS\n" + "This can be done in testing/mozbase/mozrunner/mozrunner/utils.py" + ) + self.logger.info("Allowed depth was %d" % self.maxNumRecordedFrames) + + for frames, allowed in self.foundFrames: + self.logger.lsan_leak(frames, scope=self.scope, allowed_match=allowed) + if not allowed: + failures += 1 + + if self.sawError and not ( + self.summaryData + or self.foundFrames + or self.fatalError + or self.symbolizerError + ): + self.logger.error( + "LeakSanitizer | Memory leaks detected but no leak report generated" + ) + + self.sawError = False + + return failures + + def _finishStack(self): + if self.recordMoreFrames and len(self.currStack) == 0: + self.currStack = {"unknown stack"} + if self.currStack: + self.foundFrames.add((tuple(self.currStack), self.allowedMatch)) + self.currStack = None + self.allowedMatch = None + self.recordMoreFrames = False + self.numRecordedFrames = 0 + + def _recordFrame(self, frame): + if self.allowedMatch is None and self.allowedRegexp is not None: + self.allowedMatch = frame if self.allowedRegexp.match(frame) else None + frame = self._cleanFrame(frame) + self.currStack.append(frame) + self.numRecordedFrames += 1 + if self.numRecordedFrames >= self.maxNumRecordedFrames: + self.recordMoreFrames = False + + def _cleanFrame(self, frame): + # Rust frames aren't properly demangled and in particular can contain + # some trailing junk of the form ::h[a-f0-9]{16} that changes with + # compiler versions; see bug 1507350. + return self.rustRegexp.sub("", frame) |