File: check_symbol_versioning.py

package info (click to toggle)
tuiwidgets 0.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 16,852 kB
  • sloc: cpp: 70,959; python: 655; sh: 39; makefile: 24
file content (190 lines) | stat: -rwxr-xr-x 6,995 bytes parent folder | download
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#! /usr/bin/python3
# SPDX-License-Identifier: BSL-1.0

import subprocess
import sys
import os.path
import ctypes
import ctypes.util

import elftools
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
from elftools.elf.dynamic import DynamicSection

from elftools.elf.gnuversions import GNUVerSymSection, GNUVerDefSection

proj_root = os.path.normpath(os.path.dirname(__file__) + '/..')


def symbol_version(versym, verdef, nsym):

    if nsym >= versym.num_symbols():
        raise RuntimeError('nsym out of range')

    index = versym.get_symbol(nsym).entry['ndx']

    if index in ('VER_NDX_LOCAL', 'VER_NDX_GLOBAL'):
        return None

    index = int(index) & ~0x8000

    if index <= verdef.num_versions():
        return next(verdef.get_version(index)[1]).name

    raise RuntimeError(f'version of {nsym} not in range')


def addr2line(filename, sym):
    cp = subprocess.run(['eu-addr2line', '-e', filename, sym], capture_output=True, check=False)
    if cp.stderr:
        raise RuntimeError('eu-addr2line failed with' + cp.stderr.decode())
    res = cp.stdout.decode().strip()
    if '/' in res:
        return os.path.normpath(res)

    return res

class CharP(ctypes.c_char_p):
    pass

def demangle(name):
    lib_c = ctypes.CDLL(ctypes.util.find_library('c'))
    lib_cxx = ctypes.CDLL(ctypes.util.find_library('stdc++'))
    free = lib_c.free
    free.argtypes = [ctypes.c_void_p]

    cxa_demangle = getattr(lib_cxx, '__cxa_demangle')
    cxa_demangle.restype = CharP

    status = ctypes.c_int()
    res = cxa_demangle(ctypes.c_char_p(name.encode()), None, None, ctypes.pointer(status))
    if status.value != 0:
        raise RuntimeError(f'Unexpected status {status.value} when demangling {name}')

    unmangled = res.value.decode()

    free(res)

    return unmangled


def analyze_symbols(filename, version_prefix):
    ok = True

    f = open(filename, 'rb')
    elffile = ELFFile(f)

    versym = None
    verdef = None

    for section in elffile.iter_sections():
        if isinstance(section, GNUVerSymSection):
            versym = section
        elif isinstance(section, GNUVerDefSection):
            verdef = section

    if not versym:
        raise RuntimeError('GNUVerSymSection section missing')

    if not verdef:
        raise RuntimeError('GNUVerDefSection section missing')

    symbol_tables = [s for s in elffile.iter_sections() if isinstance(s, SymbolTableSection) and s.name == '.dynsym']

    if len(symbol_tables) != 1:
        raise RuntimeError("TODO handle multiple symbol tables")
    symbol_table = symbol_tables[0]

    for nsym, symbol in enumerate(symbol_table.iter_symbols()):
        if symbol['st_info']['type'] in ('STT_FILE', 'STT_SECTION'):
            continue
        if symbol['st_info']['bind'] == 'STB_LOCAL':
            continue
        #print(symbol.name, symbol['st_other']['visibility'], symbol['st_info']['type'], symbol['st_shndx'], symbol_version(verinfo, nsym))
        if symbol['st_shndx'] != 'SHN_UNDEF':
            src = addr2line(filename, symbol.name)
            symver = symbol_version(versym, verdef, nsym)

            if src.startswith(proj_root + '/'):
                if symver is None:
                    symver = '<unversioned>'
                    ok = False

                if not symver.startswith(version_prefix):
                    print("Symbol from our source not versioned", symbol.name, symver)
                    print(f'   {src}')
                    ok = False
            elif src.startswith('/usr/include/'):
                if symbol.name.startswith(version_prefix) and symver and symver.startswith(version_prefix):
                    # TODO investigate this case, observed on arm64 qt6 build.
                    pass
                elif symbol['st_info']['bind'] != 'STB_WEAK':
                    print("Symbol from external header, but not weak", symbol.name, symver)
                    print(f'   {src}')
                    ok = False
            elif src == '??:0':
                if symver is None or not symver.startswith(version_prefix):
                    demangled_name = demangle(symbol.name)

                    external = False
                    external_names = [
                        ('std::', ['vtable for ', 'typeinfo for ', 'typeinfo name for ']),
                        ('QMetaType::registerMutableView<', ['typeinfo for ', 'typeinfo name for ']),
                        ('QMetaType::registerMutableViewImpl<', ['guard variable for ']),
                        ('QMetaType::registerConverter<', ['typeinfo for ', 'typeinfo name for ']),
                        ('QMetaType::registerConverterImpl<', ['guard variable for ']),
                        'typeinfo for QSharedData',
                        'typeinfo name for QSharedData',
                        'QtPrivate::QMetaTypeForType<',
                        'QtPrivate::QMetaTypeInterfaceWrapper<',
                        'QMetaSequence::MetaSequence<',
                        'QtPrivate::QSequentialIterableMutableViewFunctor<',
                        'QtPrivate::QSequentialIterableConvertFunctor<',
                    ]

                    for external_def in external_names:
                        if isinstance(external_def, tuple):
                            external_name = external_def[0]
                            prefixes = external_def[1]
                        else:
                            external_name = external_def
                            prefixes = []

                        if demangled_name.startswith(external_name):
                            external = True

                        for prefix in prefixes:
                            if demangled_name.startswith(prefix + external_name):
                                external = True

                    if external:
                        # STB_LOOS is used as STB_GNU_UNIQUE on linux
                        if symbol['st_info']['bind'] != 'STB_WEAK' and symbol['st_info']['bind'] != 'STB_LOOS':
                            print("Symbol from ??, but not weak", symbol.name, symver, symbol['st_info']['bind'])
                            print(f'   {src}')
                            ok = False
                    else:
                        print("Symbol from ?? not versioned", symbol.name, demangled_name, symver, symbol['st_info']['type'])
                        print(f'   {src}')
                        ok = False

                if symbol['st_info']['type'] != 'STT_OBJECT':
                    print("Symbol from ?? not of object type", symbol.name, symver, symbol['st_info']['type'])
                    print(f'   {src}')
                    ok = False

            else:
                print("Unexpected source location: ", src)
                ok = False

    return ok


def main():
    mainlib_ok = analyze_symbols(sys.argv[1], sys.argv[2])

    return 0 if mainlib_ok else 1

if __name__ == '__main__':
    sys.exit(main())