scripts: add MACHO dylib checking to symbol-check.py

This commit is contained in:
fanquake 2020-01-03 19:34:58 +08:00
parent 76bf97213f
commit c491368d8c
No known key found for this signature in database
GPG Key ID: 2EEB9F5CC09526C1
4 changed files with 128 additions and 28 deletions

View File

@ -103,17 +103,21 @@ Perform basic security checks on a series of executables.
symbol-check.py symbol-check.py
=============== ===============
A script to check that the (Linux) executables produced by gitian only contain A script to check that the executables produced by gitian only contain
allowed gcc, glibc and libstdc++ version symbols. This makes sure they are certain symbols and are only linked against allowed libraries.
still compatible with the minimum supported Linux distribution versions.
For Linux this means checking for allowed gcc, glibc and libstdc++ version symbols.
This makes sure they are still compatible with the minimum supported distribution versions.
For macOS we check that the executables are only linked against libraries we allow.
Example usage after a gitian build: Example usage after a gitian build:
find ../gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py find ../gitian-builder/build -type f -executable | xargs python3 contrib/devtools/symbol-check.py
If only supported symbols are used the return value will be 0 and the output will be empty. If no errors occur the return value will be 0 and the output will be empty.
If there are 'unsupported' symbols, the return value will be 1 a list like this will be printed: If there are any errors the return value will be 1 and output like this will be printed:
.../64/test_bitcoin: symbol memcpy from unsupported version GLIBC_2.14 .../64/test_bitcoin: symbol memcpy from unsupported version GLIBC_2.14
.../64/test_bitcoin: symbol __fdelt_chk from unsupported version GLIBC_2.15 .../64/test_bitcoin: symbol __fdelt_chk from unsupported version GLIBC_2.15

View File

@ -15,6 +15,7 @@ import subprocess
import re import re
import sys import sys
import os import os
from typing import List, Optional, Tuple
# Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases # Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases
# #
@ -52,8 +53,10 @@ IGNORE_EXPORTS = {
} }
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf') READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt') CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
OTOOL_CMD = os.getenv('OTOOL', '/usr/bin/otool')
# Allowed NEEDED libraries # Allowed NEEDED libraries
ALLOWED_LIBRARIES = { ELF_ALLOWED_LIBRARIES = {
# bitcoind and bitcoin-qt # bitcoind and bitcoin-qt
'libgcc_s.so.1', # GCC base support 'libgcc_s.so.1', # GCC base support
'libc.so.6', # C library 'libc.so.6', # C library
@ -79,6 +82,25 @@ ARCH_MIN_GLIBC_VER = {
'AArch64':(2,17), 'AArch64':(2,17),
'RISC-V': (2,27) 'RISC-V': (2,27)
} }
MACHO_ALLOWED_LIBRARIES = {
# bitcoind and bitcoin-qt
'libc++.1.dylib', # C++ Standard Library
'libSystem.B.dylib', # libc, libm, libpthread, libinfo
# bitcoin-qt only
'AppKit', # user interface
'ApplicationServices', # common application tasks.
'Carbon', # deprecated c back-compat API
'CoreFoundation', # low level func, data types
'CoreGraphics', # 2D rendering
'CoreServices', # operating system services
'CoreText', # interface for laying out text and handling fonts.
'Foundation', # base layer functionality for apps/frameworks
'ImageIO', # read and write image file formats.
'IOKit', # user-space access to hardware devices and drivers.
'libobjc.A.dylib', # Objective-C runtime library
}
class CPPFilt(object): class CPPFilt(object):
''' '''
Demangle C++ symbol names. Demangle C++ symbol names.
@ -98,15 +120,15 @@ class CPPFilt(object):
self.proc.stdout.close() self.proc.stdout.close()
self.proc.wait() self.proc.wait()
def read_symbols(executable, imports=True): def read_symbols(executable, imports=True) -> List[Tuple[str, str, str]]:
''' '''
Parse an ELF executable and return a list of (symbol,version) tuples Parse an ELF executable and return a list of (symbol,version, arch) tuples
for dynamic, imported symbols. for dynamic, imported symbols.
''' '''
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', '-h', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', '-h', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
(stdout, stderr) = p.communicate() (stdout, stderr) = p.communicate()
if p.returncode: if p.returncode:
raise IOError('Could not read symbols for %s: %s' % (executable, stderr.strip())) raise IOError('Could not read symbols for {}: {}'.format(executable, stderr.strip()))
syms = [] syms = []
for line in stdout.splitlines(): for line in stdout.splitlines():
line = line.split() line = line.split()
@ -121,7 +143,7 @@ def read_symbols(executable, imports=True):
syms.append((sym, version, arch)) syms.append((sym, version, arch))
return syms return syms
def check_version(max_versions, version, arch): def check_version(max_versions, version, arch) -> bool:
if '_' in version: if '_' in version:
(lib, _, ver) = version.rpartition('_') (lib, _, ver) = version.rpartition('_')
else: else:
@ -132,7 +154,7 @@ def check_version(max_versions, version, arch):
return False return False
return ver <= max_versions[lib] or lib == 'GLIBC' and ver <= ARCH_MIN_GLIBC_VER[arch] return ver <= max_versions[lib] or lib == 'GLIBC' and ver <= ARCH_MIN_GLIBC_VER[arch]
def read_libraries(filename): def elf_read_libraries(filename) -> List[str]:
p = subprocess.Popen([READELF_CMD, '-d', '-W', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True) p = subprocess.Popen([READELF_CMD, '-d', '-W', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
(stdout, stderr) = p.communicate() (stdout, stderr) = p.communicate()
if p.returncode: if p.returncode:
@ -148,26 +170,94 @@ def read_libraries(filename):
raise ValueError('Unparseable (NEEDED) specification') raise ValueError('Unparseable (NEEDED) specification')
return libraries return libraries
if __name__ == '__main__': def check_imported_symbols(filename) -> bool:
cppfilt = CPPFilt() cppfilt = CPPFilt()
ok = True
for sym, version, arch in read_symbols(filename, True):
if version and not check_version(MAX_VERSIONS, version, arch):
print('{}: symbol {} from unsupported version {}'.format(filename, cppfilt(sym), version))
ok = False
return ok
def check_exported_symbols(filename) -> bool:
cppfilt = CPPFilt()
ok = True
for sym,version,arch in read_symbols(filename, False):
if arch == 'RISC-V' or sym in IGNORE_EXPORTS:
continue
print('{}: export of symbol {} not allowed'.format(filename, cppfilt(sym)))
ok = False
return ok
def check_ELF_libraries(filename) -> bool:
ok = True
for library_name in elf_read_libraries(filename):
if library_name not in ELF_ALLOWED_LIBRARIES:
print('{}: NEEDED library {} is not allowed'.format(filename, library_name))
ok = False
return ok
def macho_read_libraries(filename) -> List[str]:
p = subprocess.Popen([OTOOL_CMD, '-L', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, universal_newlines=True)
(stdout, stderr) = p.communicate()
if p.returncode:
raise IOError('Error opening file')
libraries = []
for line in stdout.splitlines():
tokens = line.split()
if len(tokens) == 1: # skip executable name
continue
libraries.append(tokens[0].split('/')[-1])
return libraries
def check_MACHO_libraries(filename) -> bool:
ok = True
for dylib in macho_read_libraries(filename):
if dylib not in MACHO_ALLOWED_LIBRARIES:
print('{} is not in ALLOWED_LIBRARIES!'.format(dylib))
ok = False
return ok
CHECKS = {
'ELF': [
('IMPORTED_SYMBOLS', check_imported_symbols),
('EXPORTED_SYMBOLS', check_exported_symbols),
('LIBRARY_DEPENDENCIES', check_ELF_libraries)
],
'MACHO': [
('DYNAMIC_LIBRARIES', check_MACHO_libraries)
]
}
def identify_executable(executable) -> Optional[str]:
with open(filename, 'rb') as f:
magic = f.read(4)
if magic.startswith(b'MZ'):
return 'PE'
elif magic.startswith(b'\x7fELF'):
return 'ELF'
elif magic.startswith(b'\xcf\xfa'):
return 'MACHO'
return None
if __name__ == '__main__':
retval = 0 retval = 0
for filename in sys.argv[1:]: for filename in sys.argv[1:]:
# Check imported symbols try:
for sym,version,arch in read_symbols(filename, True): etype = identify_executable(filename)
if version and not check_version(MAX_VERSIONS, version, arch): if etype is None:
print('%s: symbol %s from unsupported version %s' % (filename, cppfilt(sym), version)) print('{}: unknown format'.format(filename))
retval = 1
# Check exported symbols
if arch != 'RISC-V':
for sym,version,arch in read_symbols(filename, False):
if sym in IGNORE_EXPORTS:
continue
print('%s: export of symbol %s not allowed' % (filename, cppfilt(sym)))
retval = 1
# Check dependency libraries
for library_name in read_libraries(filename):
if library_name not in ALLOWED_LIBRARIES:
print('%s: NEEDED library %s is not allowed' % (filename, library_name))
retval = 1 retval = 1
continue
failed = []
for (name, func) in CHECKS[etype]:
if not func(filename):
failed.append(name)
if failed:
print('{}: failed {}'.format(filename, ' '.join(failed)))
retval = 1
except IOError:
print('{}: cannot open'.format(filename))
retval = 1
sys.exit(retval) sys.exit(retval)

View File

@ -138,6 +138,7 @@ script: |
CONFIG_SITE=${BASEPREFIX}/${i}/share/config.site ./configure --prefix=/ --disable-ccache --disable-maintainer-mode --disable-dependency-tracking ${CONFIGFLAGS} CONFIG_SITE=${BASEPREFIX}/${i}/share/config.site ./configure --prefix=/ --disable-ccache --disable-maintainer-mode --disable-dependency-tracking ${CONFIGFLAGS}
make ${MAKEOPTS} make ${MAKEOPTS}
make ${MAKEOPTS} -C src check-security make ${MAKEOPTS} -C src check-security
make ${MAKEOPTS} -C src check-symbols
make install-strip DESTDIR=${INSTALLPATH} make install-strip DESTDIR=${INSTALLPATH}
make osx_volname make osx_volname

View File

@ -702,6 +702,11 @@ clean-local:
$(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@ $(AM_V_GEN) $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(CPPFLAGS) -DWINDRES_PREPROC -i $< -o $@
check-symbols: $(bin_PROGRAMS) check-symbols: $(bin_PROGRAMS)
if TARGET_DARWIN
@echo "Checking macOS dynamic libraries..."
$(AM_V_at) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)
endif
if GLIBC_BACK_COMPAT if GLIBC_BACK_COMPAT
@echo "Checking glibc back compat..." @echo "Checking glibc back compat..."
$(AM_V_at) READELF=$(READELF) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS) $(AM_V_at) READELF=$(READELF) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS)