summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozleak/mozleak/lsan.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozleak/mozleak/lsan.py')
-rw-r--r--testing/mozbase/mozleak/mozleak/lsan.py220
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)