Skip to content

Commit e165d62

Browse files
committed
maint-utils/check_symbol_versioning.py: Add script to check enough symbol versioning is applied.
1 parent cca8057 commit e165d62

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#! /usr/bin/python3
2+
# SPDX-License-Identifier: BSL-1.0
3+
4+
import subprocess
5+
import sys
6+
import os.path
7+
import ctypes
8+
import ctypes.util
9+
10+
import elftools
11+
from elftools.elf.elffile import ELFFile
12+
from elftools.elf.sections import SymbolTableSection
13+
from elftools.elf.dynamic import DynamicSection
14+
15+
from elftools.elf.gnuversions import GNUVerSymSection, GNUVerDefSection
16+
17+
proj_root = os.path.normpath(os.path.dirname(__file__) + '/..')
18+
19+
20+
def symbol_version(versym, verdef, nsym):
21+
22+
if nsym >= versym.num_symbols():
23+
raise RuntimeError('nsym out of range')
24+
25+
index = versym.get_symbol(nsym).entry['ndx']
26+
27+
if index in ('VER_NDX_LOCAL', 'VER_NDX_GLOBAL'):
28+
return None
29+
30+
index = int(index) & ~0x8000
31+
32+
if index <= verdef.num_versions():
33+
return next(verdef.get_version(index)[1]).name
34+
35+
raise RuntimeError(f'version of {nsym} not in range')
36+
37+
38+
def addr2line(filename, sym):
39+
cp = subprocess.run(['eu-addr2line', '-e', filename, sym], capture_output=True, check=False)
40+
if cp.stderr:
41+
raise RuntimeError('eu-addr2line failed with' + cp.stderr.decode())
42+
res = cp.stdout.decode().strip()
43+
if '/' in res:
44+
return os.path.normpath(res)
45+
46+
return res
47+
48+
class CharP(ctypes.c_char_p):
49+
pass
50+
51+
def demangle(name):
52+
lib_c = ctypes.CDLL(ctypes.util.find_library('c'))
53+
lib_cxx = ctypes.CDLL(ctypes.util.find_library('stdc++'))
54+
free = lib_c.free
55+
free.argtypes = [ctypes.c_void_p]
56+
57+
cxa_demangle = getattr(lib_cxx, '__cxa_demangle')
58+
cxa_demangle.restype = CharP
59+
60+
status = ctypes.c_int()
61+
res = cxa_demangle(ctypes.c_char_p(name.encode()), None, None, ctypes.pointer(status))
62+
if status.value != 0:
63+
raise RuntimeError(f'Unexpected status {status.value} when demangling {name}')
64+
65+
unmangled = res.value.decode()
66+
67+
free(res)
68+
69+
return unmangled
70+
71+
72+
def analyze_symbols(filename, version_prefix):
73+
ok = True
74+
75+
f = open(filename, 'rb')
76+
elffile = ELFFile(f)
77+
78+
versym = None
79+
verdef = None
80+
81+
for section in elffile.iter_sections():
82+
if isinstance(section, GNUVerSymSection):
83+
versym = section
84+
elif isinstance(section, GNUVerDefSection):
85+
verdef = section
86+
87+
if not versym:
88+
raise RuntimeError('GNUVerSymSection section missing')
89+
90+
if not verdef:
91+
raise RuntimeError('GNUVerDefSection section missing')
92+
93+
symbol_tables = [s for s in elffile.iter_sections() if isinstance(s, SymbolTableSection) and s.name == '.dynsym']
94+
95+
if len(symbol_tables) != 1:
96+
raise RuntimeError("TODO handle multiple symbol tables")
97+
symbol_table = symbol_tables[0]
98+
99+
for nsym, symbol in enumerate(symbol_table.iter_symbols()):
100+
if symbol['st_info']['type'] in ('STT_FILE', 'STT_SECTION'):
101+
continue
102+
if symbol['st_info']['bind'] == 'STB_LOCAL':
103+
continue
104+
#print(symbol.name, symbol['st_other']['visibility'], symbol['st_info']['type'], symbol['st_shndx'], symbol_version(verinfo, nsym))
105+
if symbol['st_shndx'] != 'SHN_UNDEF':
106+
src = addr2line(filename, symbol.name)
107+
symver = symbol_version(versym, verdef, nsym)
108+
109+
if src.startswith(proj_root + '/'):
110+
if symver is None:
111+
symver = '<unversioned>'
112+
ok = False
113+
114+
if not symver.startswith(version_prefix):
115+
print("Symbol from our source not versioned", symbol.name, symver)
116+
print(f' {src}')
117+
ok = False
118+
elif src.startswith('/usr/include/'):
119+
if symbol['st_info']['bind'] != 'STB_WEAK':
120+
print("Symbol from external header, but not weak", symbol.name, symver)
121+
print(f' {src}')
122+
ok = False
123+
elif src == '??:0':
124+
if symver is None or not symver.startswith(version_prefix):
125+
demangled_name = demangle(symbol.name)
126+
127+
external = False
128+
external_names = [
129+
('std::', ['vtable for ', 'typeinfo for ', 'typeinfo name for ']),
130+
('QMetaType::registerMutableView<', ['typeinfo for ', 'typeinfo name for ']),
131+
('QMetaType::registerMutableViewImpl<', ['guard variable for ']),
132+
('QMetaType::registerConverter<', ['typeinfo for ', 'typeinfo name for ']),
133+
('QMetaType::registerConverterImpl<', ['guard variable for ']),
134+
'typeinfo for QSharedData',
135+
'typeinfo name for QSharedData',
136+
'QtPrivate::QMetaTypeForType<',
137+
'QtPrivate::QMetaTypeInterfaceWrapper<',
138+
'QMetaSequence::MetaSequence<',
139+
'QtPrivate::QSequentialIterableMutableViewFunctor<',
140+
'QtPrivate::QSequentialIterableConvertFunctor<',
141+
]
142+
143+
for external_def in external_names:
144+
if isinstance(external_def, tuple):
145+
external_name = external_def[0]
146+
prefixes = external_def[1]
147+
else:
148+
external_name = external_def
149+
prefixes = []
150+
151+
if demangled_name.startswith(external_name):
152+
external = True
153+
154+
for prefix in prefixes:
155+
if demangled_name.startswith(prefix + external_name):
156+
external = True
157+
158+
if external:
159+
# STB_LOOS is used as STB_GNU_UNIQUE on linux
160+
if symbol['st_info']['bind'] != 'STB_WEAK' and symbol['st_info']['bind'] != 'STB_LOOS':
161+
print("Symbol from ??, but not weak", symbol.name, symver, symbol['st_info']['bind'])
162+
print(f' {src}')
163+
ok = False
164+
else:
165+
print("Symbol from ?? not versioned", symbol.name, demangled_name, symver, symbol['st_info']['type'])
166+
print(f' {src}')
167+
ok = False
168+
169+
if symbol['st_info']['type'] != 'STT_OBJECT':
170+
print("Symbol from ?? not of object type", symbol.name, symver, symbol['st_info']['type'])
171+
print(f' {src}')
172+
ok = False
173+
174+
else:
175+
print("Unexpected source location: ", src)
176+
ok = False
177+
178+
return ok
179+
180+
181+
def main():
182+
mainlib_ok = analyze_symbols(sys.argv[1], sys.argv[2])
183+
184+
return 0 if mainlib_ok else 1
185+
186+
if __name__ == '__main__':
187+
sys.exit(main())

0 commit comments

Comments
 (0)