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