tor-browser

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

reversion_glibc.py (4894B)


      1 # Copyright 2025 The Chromium Authors
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 """Rewrite incompatible default symbols in glibc.
      5 """
      6 
      7 import re
      8 import subprocess
      9 
     10 # This constant comes from the oldest glibc version in
     11 # //chrome/installer/linux/debian/dist_package_versions.json and
     12 # //chrome/installer/linux/rpm/dist_package_provides.json
     13 MAX_ALLOWED_GLIBC_VERSION = [2, 26]
     14 
     15 VERSION_PATTERN = re.compile("GLIBC_([0-9\.]+)")
     16 SECTION_PATTERN = re.compile(r"^ *\[ *[0-9]+\] +(\S+) +\S+ + ([0-9a-f]+) .*$")
     17 
     18 # Some otherwise disallowed symbols are referenced in the linux-chromeos build.
     19 # To continue supporting it, allow these symbols to remain enabled.
     20 SYMBOL_ALLOWLIST = {
     21    "fts64_close",
     22    "fts64_open",
     23    "fts64_read",
     24    "memfd_create",
     25 }
     26 
     27 
     28 def reversion_glibc(bin_file: str) -> None:
     29    # The two dictionaries below map from symbol name to
     30    # (symbol version, symbol index).
     31    #
     32    # The default version for a given symbol (which may be unsupported).
     33    default_version = {}
     34    # The max supported symbol version for a given symbol.
     35    supported_version = {}
     36 
     37    # Populate |default_version| and |supported_version| with data from readelf.
     38    stdout = subprocess.check_output(
     39        ["readelf", "--dyn-syms", "--wide", bin_file])
     40    for line in stdout.decode("utf-8").split("\n"):
     41        cols = re.split("\s+", line)
     42        # Remove localentry and next element which appears only in ppc64le
     43        # readelf output. Keeping them causes incorrect symbol parsing
     44        # leading to improper GLIBC version restrictions.
     45        if len(cols) > 7 and cols[7] == "[<localentry>:":
     46            cols.pop(7)
     47            cols.pop(7)
     48        # Skip the preamble.
     49        if len(cols) < 9:
     50            continue
     51 
     52        index = cols[1].rstrip(":")
     53        # Skip the header.
     54        if not index.isdigit():
     55            continue
     56 
     57        index = int(index)
     58        name = cols[8].split("@")
     59        # Ignore unversioned symbols.
     60        if len(name) < 2:
     61            continue
     62 
     63        base_name = name[0]
     64        version = name[-1]
     65        # The default version will have '@@' in the name.
     66        is_default = len(name) > 2
     67 
     68        if version.startswith("XCRYPT_"):
     69            # Prefer GLIBC_* versioned symbols over XCRYPT_* ones.
     70            # Set the version to something > MAX_ALLOWED_GLIBC_VERSION
     71            # so this symbol will not be picked.
     72            version = [10**10]
     73        else:
     74            match = re.match(VERSION_PATTERN, version)
     75            # Ignore symbols versioned with GLIBC_PRIVATE.
     76            if not match:
     77                continue
     78            version = [int(part) for part in match.group(1).split(".")]
     79 
     80        if version < MAX_ALLOWED_GLIBC_VERSION:
     81            old_supported_version = supported_version.get(
     82                base_name, ([-1], -1))
     83            supported_version[base_name] = max((version, index),
     84                                               old_supported_version)
     85        if is_default:
     86            default_version[base_name] = (version, index)
     87 
     88    # Get the offset into the binary of the .gnu.version section from readelf.
     89    stdout = subprocess.check_output(
     90        ["readelf", "--sections", "--wide", bin_file])
     91    for line in stdout.decode("utf-8").split("\n"):
     92        if match := SECTION_PATTERN.match(line):
     93            section_name, address = match.groups()
     94            if section_name == ".gnu.version":
     95                gnu_version_addr = int(address, base=16)
     96                break
     97    else:
     98        raise Exception("No .gnu.version section found")
     99 
    100    # Rewrite the binary.
    101    bin_data = bytearray(open(bin_file, "rb").read())
    102    for name, (version, index) in default_version.items():
    103        # No need to rewrite the default if it's already an allowed version.
    104        if version <= MAX_ALLOWED_GLIBC_VERSION:
    105            continue
    106 
    107        if name in SYMBOL_ALLOWLIST:
    108            continue
    109        elif name in supported_version:
    110            _, supported_index = supported_version[name]
    111        else:
    112            supported_index = -1
    113 
    114        # The .gnu.version section is divided into 16-bit chunks that give the
    115        # symbol versions.  The 16th bit is a flag that's false for the default
    116        # version.  The data is stored in little-endian so we need to add 1 to
    117        # get the address of the byte we want to flip.
    118        #
    119        # Disable the unsupported symbol.
    120        old_default = gnu_version_addr + 2 * index + 1
    121        assert (bin_data[old_default] & 0x80) == 0
    122        bin_data[old_default] ^= 0x80
    123 
    124        # If we found a supported version, enable that as default.
    125        if supported_index != -1:
    126            new_default = gnu_version_addr + 2 * supported_index + 1
    127            assert (bin_data[new_default] & 0x80) == 0x80
    128            bin_data[new_default] ^= 0x80
    129 
    130    open(bin_file, "wb").write(bin_data)