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)