tor-browser

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

rewrite_sanitizer_dylib.py (3934B)


      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 os
      6 import re
      7 import shutil
      8 import subprocess
      9 import sys
     10 from argparse import ArgumentParser
     11 from pathlib import Path
     12 
     13 from buildconfig import substs
     14 
     15 """
     16 Scans the given directories for binaries referencing the AddressSanitizer
     17 runtime library, copies it to the main directory.
     18 """
     19 
     20 # This is the dylib name pattern
     21 DYLIB_NAME_PATTERN = re.compile(r"libclang_rt\.(a|ub)san_osx_dynamic\.dylib")
     22 
     23 
     24 def resolve_rpath(filename):
     25    otoolOut = subprocess.check_output([substs["OTOOL"], "-l", filename], text=True)
     26    currentCmd = None
     27 
     28    # The lines we need to find look like this:
     29    # ...
     30    # Load command 22
     31    #           cmd LC_RPATH
     32    #       cmdsize 80
     33    #          path /home/build/src/clang/bin/../lib/clang/3.8.0/lib/darwin (offset 12)
     34    # Load command 23
     35    # ...
     36    # Other load command types have a varying number of fields.
     37    for line in otoolOut.splitlines():
     38        cmdMatch = re.match(r"^\s+cmd ([A-Z_]+)", line)
     39        if cmdMatch is not None:
     40            currentCmd = cmdMatch.group(1)
     41            continue
     42 
     43        if currentCmd == "LC_RPATH":
     44            pathMatch = re.match(r"^\s+path (.*) \(offset \d+\)", line)
     45            if pathMatch is not None:
     46                path = pathMatch.group(1)
     47                if Path(path).is_dir():
     48                    return path
     49 
     50    print(f"@rpath could not be resolved from {filename}", file=sys.stderr)
     51    sys.exit(1)
     52 
     53 
     54 def scan_directory(path):
     55    dylibsCopied = set()
     56    dylibsRequired = set()
     57 
     58    if not path.is_dir():
     59        print(f"Input path {path} is not a folder", file=sys.stderr)
     60        sys.exit(1)
     61 
     62    for file in path.rglob("*"):
     63        if not file.is_file():
     64            continue
     65 
     66        # Skip all files that aren't either dylibs or executable
     67        if not (file.suffix == ".dylib" or os.access(str(file), os.X_OK)):
     68            continue
     69 
     70        try:
     71            otoolOut = subprocess.check_output(
     72                [substs["OTOOL"], "-L", str(file)], text=True
     73            )
     74        except Exception:
     75            # Errors are expected on non-mach executables, ignore them and continue
     76            continue
     77 
     78        for line in otoolOut.splitlines():
     79            match = DYLIB_NAME_PATTERN.search(line)
     80 
     81            if match is not None:
     82                dylibName = match.group(0)
     83                absDylibPath = line.split()[0]
     84 
     85                dylibsRequired.add(dylibName)
     86 
     87                if dylibName not in dylibsCopied:
     88                    if absDylibPath.startswith("@rpath/"):
     89                        rpath = resolve_rpath(str(file))
     90                        copyDylibPath = absDylibPath.replace("@rpath", rpath)
     91                    else:
     92                        copyDylibPath = absDylibPath
     93 
     94                    if Path(copyDylibPath).is_file():
     95                        # Copy the runtime once to the main directory, which is passed
     96                        # as the argument to this function.
     97                        shutil.copy(copyDylibPath, str(path))
     98                        dylibsCopied.add(dylibName)
     99                    else:
    100                        print(
    101                            f"dylib path in {file} was not found at: {copyDylibPath}",
    102                            file=sys.stderr,
    103                        )
    104 
    105                break
    106 
    107    dylibsMissing = dylibsRequired - dylibsCopied
    108    if dylibsMissing:
    109        for dylibName in dylibsMissing:
    110            print(f"{dylibName} could not be found", file=sys.stderr)
    111        sys.exit(1)
    112 
    113 
    114 def parse_args(argv=None):
    115    parser = ArgumentParser()
    116    parser.add_argument("paths", metavar="path", type=Path, nargs="+")
    117    return parser.parse_args(argv)
    118 
    119 
    120 if __name__ == "__main__":
    121    args = parse_args()
    122    for d in args.paths:
    123        scan_directory(d)