tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

output_handler.py (5282B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import logging
      6 import re
      7 
      8 
      9 class OutputHandler:
     10    """
     11    A class for handling Valgrind output.
     12 
     13    Valgrind errors look like this:
     14 
     15    ==60741== 40 (24 direct, 16 indirect) bytes in 1 blocks are definitely lost in loss record 2,746 of 5,235
     16    ==60741==    at 0x4C26B43: calloc (vg_replace_malloc.c:593)
     17    ==60741==    by 0x63AEF65: PR_Calloc (prmem.c:443)
     18    ==60741==    by 0x69F236E: PORT_ZAlloc_Util (secport.c:117)
     19    ==60741==    by 0x69F1336: SECITEM_AllocItem_Util (secitem.c:28)
     20    ==60741==    by 0xA04280B: ffi_call_unix64 (in /builds/slave/m-in-l64-valgrind-000000000000/objdir/toolkit/library/libxul.so)
     21    ==60741==    by 0xA042443: ffi_call (ffi64.c:485)
     22 
     23    For each such error, this class extracts most or all of the first (error
     24    kind) line, plus the function name in each of the first few stack entries.
     25    With this data it constructs and prints a TEST-UNEXPECTED-FAIL message that
     26    TBPL will highlight.
     27 
     28    It buffers these lines from which text is extracted so that the
     29    TEST-UNEXPECTED-FAIL message can be printed before the full error.
     30 
     31    Parsing the Valgrind output isn't ideal, and it may break in the future if
     32    Valgrind changes the format of the messages, or introduces new error kinds.
     33    To protect against this, we also count how many lines containing
     34    "<insert_a_suppression_name_here>" are seen. Thanks to the use of
     35    --gen-suppressions=yes, exactly one of these lines is present per error. If
     36    the count of these lines doesn't match the error count found during
     37    parsing, then the parsing has missed one or more errors and we can fail
     38    appropriately.
     39    """  # NOQA: E501
     40 
     41    def __init__(self, logger):
     42        # The regexps in this list match all of Valgrind's errors. Note that
     43        # Valgrind is English-only, so we don't have to worry about
     44        # localization.
     45        self.logger = logger
     46        self.re_error = (
     47            r"==\d+== ("
     48            + r"(Use of uninitialised value of size \d+)|"
     49            + r"(Conditional jump or move depends on uninitialised value\(s\))|"
     50            + r"(Syscall param .* contains uninitialised byte\(s\))|"
     51            + r"(Syscall param .* points to (unaddressable|uninitialised) byte\(s\))|"
     52            + r"((Unaddressable|Uninitialised) byte\(s\) found during client check request)|"
     53            + r"(Invalid free\(\) / delete / delete\[\] / realloc\(\))|"
     54            + r"(Mismatched free\(\) / delete / delete \[\])|"
     55            + r"(Invalid (read|write) of size \d+)|"
     56            + r"(Jump to the invalid address stated on the next line)|"
     57            + r"(Source and destination overlap in .*)|"
     58            + r"(.* bytes in .* blocks are .* lost)"
     59            + r")"
     60        )
     61        # Match identifer chars, plus ':' for namespaces, and '\?' in order to
     62        # match "???" which Valgrind sometimes produces.
     63        self.re_stack_entry = r"^==\d+==.*0x[A-Z0-9]+: ([A-Za-z0-9_:\?]+)"
     64        self.re_suppression = r" *<insert_a_suppression_name_here>"
     65        self.error_count = 0
     66        self.suppression_count = 0
     67        self.number_of_stack_entries_to_get = 0
     68        self.curr_error = None
     69        self.curr_location = None
     70        self.buffered_lines = None
     71 
     72    def log(self, line):
     73        self.logger(logging.INFO, "valgrind-output", {"line": line}, "{line}")
     74 
     75    def __call__(self, line):
     76        if self.number_of_stack_entries_to_get == 0:
     77            # Look for the start of a Valgrind error.
     78            m = re.search(self.re_error, line)
     79            if m:
     80                self.error_count += 1
     81                self.number_of_stack_entries_to_get = 4
     82                self.curr_error = m.group(1)
     83                self.curr_location = ""
     84                self.buffered_lines = [line]
     85            else:
     86                self.log(line)
     87 
     88        else:
     89            # We've recently found a Valgrind error, and are now extracting
     90            # details from the first few stack entries.
     91            self.buffered_lines.append(line)
     92            m = re.match(self.re_stack_entry, line)
     93            if m:
     94                self.curr_location += m.group(1)
     95            else:
     96                self.curr_location += "?!?"
     97 
     98            self.number_of_stack_entries_to_get -= 1
     99            if self.number_of_stack_entries_to_get != 0:
    100                self.curr_location += " / "
    101            else:
    102                # We've finished getting the first few stack entries. Print the
    103                # failure message and the buffered lines, and then reset state.
    104                self.logger(
    105                    logging.ERROR,
    106                    "valgrind-error-msg",
    107                    {"error": self.curr_error, "location": self.curr_location},
    108                    "TEST-UNEXPECTED-FAIL | valgrind-test | {error} at {location}",
    109                )
    110                for b in self.buffered_lines:
    111                    self.log(b)
    112                self.curr_error = None
    113                self.curr_location = None
    114                self.buffered_lines = None
    115 
    116        if re.match(self.re_suppression, line):
    117            self.suppression_count += 1