File: reversion_glibc.py

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (135 lines) | stat: -rw-r--r-- 5,075 bytes parent folder | download | duplicates (5)
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)