1515import re
1616import sys
1717import os
18+ from typing import List , Optional , Tuple
1819
1920# Debian 8 (Jessie) EOL: 2020. https://wiki.debian.org/DebianReleases#Production_Releases
2021#
5253}
5354READELF_CMD = os .getenv ('READELF' , '/usr/bin/readelf' )
5455CPPFILT_CMD = os .getenv ('CPPFILT' , '/usr/bin/c++filt' )
56+ OTOOL_CMD = os .getenv ('OTOOL' , '/usr/bin/otool' )
57+
5558# Allowed NEEDED libraries
56- ALLOWED_LIBRARIES = {
59+ ELF_ALLOWED_LIBRARIES = {
5760# bitcoind and bitcoin-qt
5861'libgcc_s.so.1' , # GCC base support
5962'libc.so.6' , # C library
8083'AArch64' :(2 ,17 ),
8184'RISC-V' : (2 ,27 )
8285}
86+
87+ MACHO_ALLOWED_LIBRARIES = {
88+ # bitcoind and bitcoin-qt
89+ 'libc++.1.dylib' , # C++ Standard Library
90+ 'libSystem.B.dylib' , # libc, libm, libpthread, libinfo
91+ # bitcoin-qt only
92+ 'AGL' ,
93+ 'AppKit' , # user interface
94+ 'ApplicationServices' , # common application tasks.
95+ 'Carbon' , # deprecated c back-compat API
96+ 'CFNetwork' ,
97+ 'CoreFoundation' , # low level func, data types
98+ 'CoreGraphics' , # 2D rendering
99+ 'CoreServices' , # operating system services
100+ 'CoreText' , # interface for laying out text and handling fonts.
101+ 'DiskArbitration' ,
102+ 'Foundation' , # base layer functionality for apps/frameworks
103+ 'ImageIO' , # read and write image file formats.
104+ 'IOKit' , # user-space access to hardware devices and drivers.
105+ 'libobjc.A.dylib' , # Objective-C runtime library
106+ 'OpenGL' ,
107+ 'Security' ,
108+ 'SystemConfiguration' ,
109+ }
110+
83111class CPPFilt (object ):
84112 '''
85113 Demangle C++ symbol names.
@@ -99,15 +127,15 @@ def close(self):
99127 self .proc .stdout .close ()
100128 self .proc .wait ()
101129
102- def read_symbols (executable , imports = True ):
130+ def read_symbols (executable , imports = True ) -> List [ Tuple [ str , str , str ]] :
103131 '''
104- Parse an ELF executable and return a list of (symbol,version) tuples
132+ Parse an ELF executable and return a list of (symbol,version, arch ) tuples
105133 for dynamic, imported symbols.
106134 '''
107135 p = subprocess .Popen ([READELF_CMD , '--dyn-syms' , '-W' , '-h' , executable ], stdout = subprocess .PIPE , stderr = subprocess .PIPE , stdin = subprocess .PIPE , universal_newlines = True )
108136 (stdout , stderr ) = p .communicate ()
109137 if p .returncode :
110- raise IOError ('Could not read symbols for %s: %s' % (executable , stderr .strip ()))
138+ raise IOError ('Could not read symbols for {}: {}' . format (executable , stderr .strip ()))
111139 syms = []
112140 for line in stdout .splitlines ():
113141 line = line .split ()
@@ -122,7 +150,7 @@ def read_symbols(executable, imports=True):
122150 syms .append ((sym , version , arch ))
123151 return syms
124152
125- def check_version (max_versions , version , arch ):
153+ def check_version (max_versions , version , arch ) -> bool :
126154 if '_' in version :
127155 (lib , _ , ver ) = version .rpartition ('_' )
128156 else :
@@ -133,7 +161,7 @@ def check_version(max_versions, version, arch):
133161 return False
134162 return ver <= max_versions [lib ] or lib == 'GLIBC' and ver <= ARCH_MIN_GLIBC_VER [arch ]
135163
136- def read_libraries (filename ):
164+ def elf_read_libraries (filename ) -> List [ str ] :
137165 p = subprocess .Popen ([READELF_CMD , '-d' , '-W' , filename ], stdout = subprocess .PIPE , stderr = subprocess .PIPE , stdin = subprocess .PIPE , universal_newlines = True )
138166 (stdout , stderr ) = p .communicate ()
139167 if p .returncode :
@@ -149,26 +177,94 @@ def read_libraries(filename):
149177 raise ValueError ('Unparseable (NEEDED) specification' )
150178 return libraries
151179
152- if __name__ == '__main__' :
180+ def check_imported_symbols ( filename ) -> bool :
153181 cppfilt = CPPFilt ()
182+ ok = True
183+ for sym , version , arch in read_symbols (filename , True ):
184+ if version and not check_version (MAX_VERSIONS , version , arch ):
185+ print ('{}: symbol {} from unsupported version {}' .format (filename , cppfilt (sym ), version ))
186+ ok = False
187+ return ok
188+
189+ def check_exported_symbols (filename ) -> bool :
190+ cppfilt = CPPFilt ()
191+ ok = True
192+ for sym ,version ,arch in read_symbols (filename , False ):
193+ if arch == 'RISC-V' or sym in IGNORE_EXPORTS :
194+ continue
195+ print ('{}: export of symbol {} not allowed' .format (filename , cppfilt (sym )))
196+ ok = False
197+ return ok
198+
199+ def check_ELF_libraries (filename ) -> bool :
200+ ok = True
201+ for library_name in elf_read_libraries (filename ):
202+ if library_name not in ELF_ALLOWED_LIBRARIES :
203+ print ('{}: NEEDED library {} is not allowed' .format (filename , library_name ))
204+ ok = False
205+ return ok
206+
207+ def macho_read_libraries (filename ) -> List [str ]:
208+ p = subprocess .Popen ([OTOOL_CMD , '-L' , filename ], stdout = subprocess .PIPE , stderr = subprocess .PIPE , stdin = subprocess .PIPE , universal_newlines = True )
209+ (stdout , stderr ) = p .communicate ()
210+ if p .returncode :
211+ raise IOError ('Error opening file' )
212+ libraries = []
213+ for line in stdout .splitlines ():
214+ tokens = line .split ()
215+ if len (tokens ) == 1 : # skip executable name
216+ continue
217+ libraries .append (tokens [0 ].split ('/' )[- 1 ])
218+ return libraries
219+
220+ def check_MACHO_libraries (filename ) -> bool :
221+ ok = True
222+ for dylib in macho_read_libraries (filename ):
223+ if dylib not in MACHO_ALLOWED_LIBRARIES :
224+ print ('{} is not in ALLOWED_LIBRARIES!' .format (dylib ))
225+ ok = False
226+ return ok
227+
228+ CHECKS = {
229+ 'ELF' : [
230+ ('IMPORTED_SYMBOLS' , check_imported_symbols ),
231+ ('EXPORTED_SYMBOLS' , check_exported_symbols ),
232+ ('LIBRARY_DEPENDENCIES' , check_ELF_libraries )
233+ ],
234+ 'MACHO' : [
235+ ('DYNAMIC_LIBRARIES' , check_MACHO_libraries )
236+ ]
237+ }
238+
239+ def identify_executable (executable ) -> Optional [str ]:
240+ with open (filename , 'rb' ) as f :
241+ magic = f .read (4 )
242+ if magic .startswith (b'MZ' ):
243+ return 'PE'
244+ elif magic .startswith (b'\x7f ELF' ):
245+ return 'ELF'
246+ elif magic .startswith (b'\xcf \xfa ' ):
247+ return 'MACHO'
248+ return None
249+
250+ if __name__ == '__main__' :
154251 retval = 0
155252 for filename in sys .argv [1 :]:
156- # Check imported symbols
157- for sym ,version ,arch in read_symbols (filename , True ):
158- if version and not check_version (MAX_VERSIONS , version , arch ):
159- print ('%s: symbol %s from unsupported version %s' % (filename , cppfilt (sym ), version ))
160- retval = 1
161- # Check exported symbols
162- if arch != 'RISC-V' :
163- for sym ,version ,arch in read_symbols (filename , False ):
164- if sym in IGNORE_EXPORTS :
165- continue
166- print ('%s: export of symbol %s not allowed' % (filename , cppfilt (sym )))
167- retval = 1
168- # Check dependency libraries
169- for library_name in read_libraries (filename ):
170- if library_name not in ALLOWED_LIBRARIES :
171- print ('%s: NEEDED library %s is not allowed' % (filename , library_name ))
253+ try :
254+ etype = identify_executable (filename )
255+ if etype is None :
256+ print ('{}: unknown format' .format (filename ))
172257 retval = 1
258+ continue
173259
260+ failed = []
261+ for (name , func ) in CHECKS [etype ]:
262+ if not func (filename ):
263+ failed .append (name )
264+ if failed :
265+ print ('{}: failed {}' .format (filename , ' ' .join (failed )))
266+ retval = 1
267+ except IOError :
268+ print ('{}: cannot open' .format (filename ))
269+ retval = 1
174270 sys .exit (retval )
0 commit comments