class BinariesCheck(AbstractCheck): """ Checks for binary files in the package. """ srcname_regex = re.compile(r'(.*?)-[0-9]') validso_regex = re.compile(r'(\.so\.\d+(\.\d+)*|\d\.so)$') soversion_regex = re.compile( r'.*?([0-9][.0-9]*)\.so|.*\.so\.([0-9][.0-9]*).*') usr_lib_regex = re.compile(r'^/usr/lib(64)?/') ldso_soname_regex = re.compile(r'^ld(-linux(-(ia|x86_)64))?\.so') numeric_dir_regex = re.compile( r'/usr(?:/share)/man/man./(.*)\.[0-9](?:\.gz|\.bz2)') versioned_dir_regex = re.compile(r'[^.][0-9]') so_regex = re.compile(r'/lib(64)?/[^/]+\.so(\.[0-9]+)*$') bin_regex = re.compile(r'^(/usr(/X11R6)?)?/s?bin/') la_file_regex = re.compile(r'\.la$') invalid_dir_ref_regex = re.compile(r'/(home|tmp)(\W|$)') usr_arch_share_regex = re.compile( r'/share/.*/(?:x86|i.86|x86_64|ppc|ppc64|s390|s390x|ia64|m68k|arm|aarch64|mips|riscv)' ) def __init__(self, config, output): super().__init__(config, output) self.is_exec = False self.is_shobj = False self.system_lib_paths = config.configuration['SystemLibPaths'] pie_exec_re = config.configuration['PieExecutables'] self.pie_exec_re = re.compile(pie_exec_re) if pie_exec_re else None self.usr_lib_exception_regex = re.compile( config.configuration['UsrLibBinaryException']) self.setgid_call_regex = self.create_regexp_call(r'set(?:res|e)?gid') self.setuid_call_regex = self.create_regexp_call(r'set(?:res|e)?uid') self.setgroups_call_regex = self.create_regexp_call( r'(?:ini|se)tgroups') self.mktemp_call_regex = self.create_regexp_call('mktemp') self.gethostbyname_call_regex = self.create_regexp_call( r'(gethostbyname|gethostbyname2|gethostbyaddr|gethostbyname_r|gethostbyname2_r|gethostbyaddr_r)' ) # register all check functions self.check_functions = [ self._check_lto_section, self._check_no_text_in_archive, self._check_missing_symtab_in_archive, self._check_missing_debug_info_in_archive, self._check_executable_stack, self._check_shared_library, self._check_dependency, self._check_library_dependency_location, self._check_security_functions, self._check_rpath, self._check_library_dependency, self._check_forbidden_functions, self._check_executable_shlib ] @staticmethod def create_nonlibc_regexp_call(call): r = r'(%s)\s?.*$' % call return re.compile(r) @staticmethod def create_regexp_call(call): r = r'(%s(?:@GLIBC\S+)?)(?:\s|$)' % call return re.compile(r) def _check_libtool_wrapper(self, pkg, fname, pkgfile): """ Print an error if the fname file contains a libtool wrapper shell script. """ if 'shell script' in pkgfile.magic: file_start = None try: with open(pkgfile.path, 'rb') as inputf: file_start = inputf.read(2048) except IOError: pass if (file_start and b'This wrapper script should never ' b'be moved out of the build directory' in file_start): self.output.add_info('E', pkg, 'libtool-wrapper-in-package', fname) def _check_invalid_la_file(self, pkg, fname): """ Check if the fname is an .la file and contains a reference to the invalid directories ('/tmp' or '/home'). If so then print a corresponding error with the matching line numbers. """ if self.la_file_regex.search(fname): lines = pkg.grep(self.invalid_dir_ref_regex, fname) if lines: self.output.add_info('E', pkg, 'invalid-la-file', fname, '(line %s)' % ', '.join(lines)) def _check_binary_in_noarch(self, pkg, bin_name): """ Print an error if the binary file bin_name is in the noarch package. """ if pkg.arch == 'noarch': self.output.add_info( 'E', pkg, 'arch-independent-package-contains-binary-or-object', bin_name) def _check_binary_in_usr_share(self, pkg, bin_name): """ Print an error if binary file bin_name is installed in /usr/share. We suppose that the package is arch dependent. """ if bin_name.startswith('/usr/share/') and \ not self.usr_arch_share_regex.search(bin_name): self.output.add_info('E', pkg, 'arch-dependent-file-in-usr-share', bin_name) def _check_binary_in_etc(self, pkg, bin_name): """ Print an error if binary file bin_name is installed in /etc directory. We suppose that the package is arch dependent. """ if bin_name.startswith('/etc/'): self.output.add_info('E', pkg, 'binary-in-etc', bin_name) def _check_unstripped_binary(self, bin_name, pkg, pkgfile): """ Print a warning if the bin_name binary has unstripped debug symbols. We suppose that the package is arch dependent and bin_name is not ocaml native, lua bytecode, .o or .static. """ if 'not stripped' in pkgfile.magic: self.output.add_info('W', pkg, 'unstripped-binary-or-object', bin_name) def _check_non_pie(self, pkg, bin_name, is_pie_exec): """ Check if the bin_name binary is built with PIE. Print an error message if it's not while PIE is forced in configuration. Print a warning if it's not forced. We suppose that the package is arch dependent and bin_name is binary executable. """ if not self.is_shobj and not is_pie_exec: if self.pie_exec_re and self.pie_exec_re.search(bin_name): self.output.add_info('E', pkg, 'non-position-independent-executable', bin_name) else: self.output.add_info( 'W', pkg, 'position-independent-executable-suggested', bin_name) def _check_exec_in_library(self, pkg, has_lib, exec_files): """ Check if the library package has an executable file installed. Print an error for every such file. """ if has_lib: for f in exec_files: self.output.add_info('E', pkg, 'executable-in-library-package', f) def _check_non_versioned(self, pkg, has_lib, files, exec_files): """ Check if the library package contains library files in non-versioned directories. Print an error for every such file. """ if has_lib: for f in files: res = self.numeric_dir_regex.search(f) fn = res and res.group(1) or f if f not in exec_files and not self.so_regex.search(f) and \ not self.versioned_dir_regex.search(fn): self.output.add_info( 'E', pkg, 'non-versioned-file-in-library-package', f) def _check_no_binary(self, pkg, has_binary, multi_pkg, has_file_in_lib64): """ Check if the arch dependent package contains any binaries. Print an error if there is no binary and it's not noarch. """ if not has_binary and not multi_pkg and not has_file_in_lib64 and \ pkg.arch != 'noarch': self.output.add_info('E', pkg, 'no-binary') def _check_noarch_with_lib64(self, pkg, has_file_in_lib64): """ Print an error if we have a noarch package that contains a file in /usr/lib64. """ if pkg.arch == 'noarch' and has_file_in_lib64: self.output.add_info('E', pkg, 'noarch-with-lib64') def _check_only_non_binary_in_usrlib(self, pkg, has_usr_lib_file, has_binary_in_usr_lib): """ Check and print a warning if we have _only_ non-binary files in the '/usr/lib'. Note: non-binaries allowed via UsrLibBinaryException config option are considered binaries. """ if has_usr_lib_file and not has_binary_in_usr_lib: self.output.add_info('W', pkg, 'only-non-binary-in-usr-lib') def _check_no_text_in_archive(self, pkg, pkgfile_path, path): """ For an archive, test if any .text sections is empty. """ if self.readelf_parser.is_archive: for comment in self.readelf_parser.comment_section_info.comments: if comment.startswith('GHC '): return for elf_file in self.readelf_parser.section_info.elf_files: code_in_text = False for section in elf_file: sn = section.name if ((sn == '.preinit_array' or sn == '.init_array' or sn == '.fini_array' or sn.startswith('.text')) and section.size > 0): code_in_text = True break if not code_in_text: self.output.add_info('E', pkg, 'lto-no-text-in-archive', path) return def _check_missing_symtab_in_archive(self, pkg, pkgfile_path, path): """ FIXME Add test coverage. """ if self.readelf_parser.is_archive: for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: if section.name == '.symtab': return self.output.add_info('E', pkg, 'static-library-without-symtab', path) def _check_missing_debug_info_in_archive(self, pkg, pkgfile_path, path): if self.readelf_parser.is_archive: for elf_file in self.readelf_parser.section_info.elf_files: has_debug_info = any('.debug_' in section.name for section in elf_file) if not has_debug_info: self.output.add_info('E', pkg, 'static-library-without-debuginfo', path) return # Check for LTO sections def _check_lto_section(self, pkg, pkgfile_path, path): for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: if '.gnu.lto_.' in section.name: self.output.add_info('E', pkg, 'lto-bytecode', path) return def _check_executable_stack(self, pkg, pkgfile_path, path): """ Check if the stack is declared as executable which is usually an error. FIXME Add test coverage. """ if not self.readelf_parser.is_archive: stack_headers = [ h for h in self.readelf_parser.program_header_info.headers if h.name == 'GNU_STACK' ] if not stack_headers: self.output.add_info('E', pkg, 'missing-PT_GNU_STACK-section', path) elif 'E' in stack_headers[0].flags: self.output.add_info('E', pkg, 'executable-stack', path) def _check_soname_symlink(self, pkg, shlib, soname): """ Check that we have a symlink with the soname in the package and it points to the checked shared library. Print an error if the symlink is invalid or missing. """ path = Path(shlib) symlink = path.parent / soname try: link = pkg.files()[str(symlink)].linkto if link not in (shlib, path.parent, ''): self.output.add_info('E', pkg, 'invalid-ldconfig-symlink', shlib, link) except KeyError: # if we do not have a symlink, report an issue if path.name.startswith('lib') or path.name.startswith('ld-'): self.output.add_info('E', pkg, 'no-ldconfig-symlink', shlib) def _check_shared_library(self, pkg, pkgfile_path, path): """ Various checks for the shared library. 1) Print 'no-soname' warning it the library has no soname present. 2) Print 'invalid-soname' error if the soname is not valid. 3) Print 'incoherent-version-in-name' error when the library major version is not present in the package name. 4) Print 'shlib-with-non-pic-code' error if the library contains object code that was compiled without -fPIC. """ if not self.readelf_parser.is_shlib: return soname = self.readelf_parser.dynamic_section_info.soname if not soname: self.output.add_info('W', pkg, 'no-soname', path) else: if not self.validso_regex.search(soname): self.output.add_info('E', pkg, 'invalid-soname', path, soname) else: self._check_soname_symlink(pkg, path, soname) # check if the major version of the library is in the package # name res = self.soversion_regex.search(soname) if res: soversion = res.group(1) or res.group(2) if soversion and soversion not in pkg.name: self.output.add_info('E', pkg, 'incoherent-version-in-name', soversion) # check if the object code in the library is compiled with PIC if not self.readelf_parser.section_info.pic: self.output.add_info('E', pkg, 'shlib-with-non-pic-code', path) def _check_dependency(self, pkg, pkgfile_path, path): """ FIXME Add test coverage. """ # following issues are errors for shared libs and warnings for executables if not self.readelf_parser.is_archive and not self.readelf_parser.is_debug: info_type = 'E' if self.readelf_parser.is_shlib else 'W' for symbol in self.ldd_parser.undefined_symbols: self.output.add_info(info_type, pkg, 'undefined-non-weak-symbol', path, symbol) for dependency in self.ldd_parser.unused_dependencies: self.output.add_info(info_type, pkg, 'unused-direct-shlib-dependency', path, dependency) def _check_library_dependency_location(self, pkg, pkgfile_path, path): """ FIXME Add test coverage. """ if not self.readelf_parser.is_archive: for dependency in self.ldd_parser.dependencies: if dependency.startswith('/opt/'): self.output.add_info('E', pkg, 'linked-against-opt-library', path, dependency) break nonusr = ('/bin', '/lib', '/sbin') if path.startswith(nonusr): for dependency in self.ldd_parser.dependencies: if dependency.startswith('/usr/'): self.output.add_info('W', pkg, 'linked-against-usr-library', path, dependency) break def _check_security_functions(self, pkg, pkgfile_path, path): setgid = any( self.readelf_parser.symbol_table_info.get_functions_for_regex( self.setgid_call_regex)) setuid = any( self.readelf_parser.symbol_table_info.get_functions_for_regex( self.setuid_call_regex)) setgroups = any( self.readelf_parser.symbol_table_info.get_functions_for_regex( self.setgroups_call_regex)) mktemp = any( self.readelf_parser.symbol_table_info.get_functions_for_regex( self.mktemp_call_regex)) gethostbyname = any( self.readelf_parser.symbol_table_info.get_functions_for_regex( self.gethostbyname_call_regex)) if setgid and setuid and not setgroups: self.output.add_info('E', pkg, 'missing-call-to-setgroups-before-setuid', path) if mktemp: self.output.add_info('E', pkg, 'call-to-mktemp', path) if gethostbyname: self.output.add_info('W', pkg, 'binary-or-shlib-calls-gethostbyname', path) def _check_rpath(self, pkg, pkgfile_path, path): for runpath in self.readelf_parser.dynamic_section_info.runpath: if runpath in self.system_lib_paths or not self.usr_lib_regex.search( runpath): self.output.add_info('E', pkg, 'binary-or-shlib-defines-rpath', path, runpath) return def _check_library_dependency(self, pkg, pkgfile_path, path): dyn_section = self.readelf_parser.dynamic_section_info if not len(dyn_section.needed) and not ( dyn_section.soname and self.ldso_soname_regex.search(dyn_section.soname)): if self.is_shobj: msg = 'shared-library-without-dependency-information' else: msg = 'statically-linked-binary' self.output.add_info('E', pkg, msg, path) else: # linked against libc ? if 'libc.' not in dyn_section.runpath and \ (not dyn_section.soname or ('libc.' not in dyn_section.soname and not self.ldso_soname_regex.search(dyn_section.soname))): for lib in dyn_section.needed: if 'libc.' in lib: return if self.is_shobj: msg = 'library-not-linked-against-libc' else: msg = 'program-not-linked-against-libc' self.output.add_info('W', pkg, msg, path) def _check_forbidden_functions(self, pkg, pkgfile_path, path): forbidden_functions = self.config.configuration['WarnOnFunction'] if forbidden_functions: for name, func in forbidden_functions.items(): # precompile regexps f_name = func['f_name'] func['f_regex'] = self.create_nonlibc_regexp_call(f_name) if 'good_param' in func and func['good_param']: func['waiver_regex'] = re.compile(func['good_param']) # register descriptions self.output.error_details.update({name: func['description']}) forbidden_calls = [] for r_name, func in forbidden_functions.items(): if any( self.readelf_parser.symbol_table_info. get_functions_for_regex(func['f_regex'])): forbidden_calls.append(r_name) if not forbidden_calls: return strings_parser = StringsParser(pkgfile_path) failed_reason = strings_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'strings-failed', path, failed_reason) return forbidden_functions_filtered = [] for fn in forbidden_calls: f = forbidden_functions[fn] if 'waiver_regex' not in f: forbidden_functions_filtered.append(fn) continue waiver = any( map(lambda string: f['waiver_regex'].search(string), strings_parser.strings)) if not waiver: forbidden_functions_filtered.append(fn) for fn in forbidden_functions_filtered: self.output.add_info('W', pkg, fn, path, forbidden_functions[fn]['f_name']) def _check_executable_shlib(self, pkg, pkgfile_path, path): if not self.is_exec and self.readelf_parser.is_shlib: interp = [ h for h in self.readelf_parser.program_header_info.headers if h.name == 'INTERP' ] if interp: self.output.add_info('E', pkg, 'shared-library-not-executable', path) def run_elf_checks(self, pkg, pkgfile_path, path): self.readelf_parser = ReadelfParser(pkgfile_path, path) failed_reason = self.readelf_parser.parsing_failed_reason() if failed_reason: self.output.add_info('E', pkg, 'readelf-failed', path, failed_reason) return if not self.readelf_parser.is_archive: self.ldd_parser = LddParser(pkgfile_path, path) failed_reason = self.ldd_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'ldd-failed', path, failed_reason) return with concurrent.futures.ThreadPoolExecutor() as executor: futures = [] for fn in self.check_functions: futures.append(executor.submit(fn, pkg, pkgfile_path, path)) concurrent.futures.wait(futures) def check_binary(self, pkg): files = pkg.files() exec_files = [] multi_pkg = False pkg_has_lib = False pkg_has_binary = False pkg_has_binary_in_usrlib = False pkg_has_usrlib_file = False pkg_has_file_in_lib64 = False # go through the all files, run files checks and collect data that are # needed later for fname, pkgfile in files.items(): # Common tests first self._check_libtool_wrapper(pkg, fname, pkgfile) self._check_invalid_la_file(pkg, fname) # consider non-binary in /usr/lib/ that is allowed by # UsrLibBinaryException config option as a "fake" binary and # do not throw 'only-non-binary-in-usr-lib' warning then if not stat.S_ISDIR( pkgfile.mode) and self.usr_lib_regex.search(fname): pkg_has_usrlib_file = True if not pkg_has_binary_in_usrlib and \ self.usr_lib_exception_regex.search(fname): # Fake that we have binaries there to avoid # only-non-binary-in-usr-lib false positives pkg_has_binary_in_usrlib = True # find out if we have a file in /usr/lib64/ directory (needed later # for the package checks) if stat.S_ISREG(pkgfile.mode) and \ (fname.startswith('/usr/lib64') or fname.startswith('/lib64')): pkg_has_file_in_lib64 = True # skip the rest of the tests for non-binaries # binary files only from here on is_ocaml_native = 'Objective caml native' in pkgfile.magic is_lua_bytecode = 'Lua bytecode' in pkgfile.magic if not (pkgfile.magic.startswith('ELF ') or 'current ar archive' in pkgfile.magic or is_ocaml_native or is_lua_bytecode): continue # mark this package as a one that has binary file pkg_has_binary = True # if there is a binary in /usr/lib then mark this package # accordingly if pkg_has_usrlib_file and not pkg_has_binary_in_usrlib and \ self.usr_lib_regex.search(fname): pkg_has_binary_in_usrlib = True self._check_binary_in_noarch(pkg, fname) # skip the rest of the tests for noarch packages # arch dependent packages only from here on if pkg.arch == 'noarch': continue self._check_binary_in_usr_share(pkg, fname) self._check_binary_in_etc(pkg, fname) # skip the rest of the tests for ocaml native, bytecode, .o and # .static if is_ocaml_native or is_lua_bytecode or fname.endswith('.o') or \ fname.endswith('.static'): continue self._check_unstripped_binary(fname, pkg, pkgfile) self.is_exec = 'executable' in pkgfile.magic self.is_shobj = 'shared object' in pkgfile.magic is_pie_exec = 'pie executable' in pkgfile.magic # run ELF checks self.run_elf_checks(pkg, pkgfile.path, fname) # inspect binary file is_shlib = self.readelf_parser.is_shlib if is_shlib: pkg_has_lib = True # skip non-exec and non-SO # executables and shared objects only from here on if not self.is_exec and not self.is_shobj: continue if self.is_shobj and not self.is_exec and '.so' not in fname and \ self.bin_regex.search(fname): # pkgfile.magic does not contain 'executable' for PIEs self.is_exec = True if self.is_exec: # add to the list of the all exec files if self.bin_regex.search(fname): exec_files.append(fname) self._check_non_pie(pkg, fname, is_pie_exec) # find out if we have a multi-package srpm = pkg[rpm.RPMTAG_SOURCERPM] if srpm: srcname = self.srcname_regex.search(srpm) if srcname: multi_pkg = (pkg.name != srcname.group(1)) # run checks for the whole package # it uses data collected in the previous for-cycle self._check_exec_in_library(pkg, pkg_has_lib, exec_files) self._check_non_versioned(pkg, pkg_has_lib, files, exec_files) self._check_no_binary(pkg, pkg_has_binary, multi_pkg, pkg_has_file_in_lib64) self._check_noarch_with_lib64(pkg, pkg_has_file_in_lib64) self._check_only_non_binary_in_usrlib(pkg, pkg_has_usrlib_file, pkg_has_binary_in_usrlib)
class BinariesCheck(AbstractCheck): """ Checks for binary files in the package. """ validso_regex = re.compile(r'(\.so\.\d+(\.\d+)*|\d\.so)$') soversion_regex = re.compile(r'.*?(-(?P<pkgversion>[0-9][.0-9]*))?\.so(\.(?P<soversion>[0-9][.0-9]*))?') usr_lib_regex = re.compile(r'^/usr/lib(64)?/') ldso_soname_regex = re.compile(r'^ld(-linux(-(ia|x86_)64))?\.so') numeric_dir_regex = re.compile(r'/usr(?:/share)/man/man./(.*)\.[0-9](?:\.gz|\.bz2)') versioned_dir_regex = re.compile(r'[^.][0-9]') so_regex = re.compile(r'/lib(64)?/[^/]+\.so(\.[0-9]+)*$') bin_regex = re.compile(r'^(/usr(/X11R6)?)?/s?bin/') la_file_regex = re.compile(r'\.la$') invalid_dir_ref_regex = re.compile(r'/(home|tmp)(\W|$)') usr_arch_share_regex = re.compile(r'/share/.*/(?:x86|i.86|x86_64|ppc|ppc64|s390|s390x|ia64|m68k|arm|aarch64|mips|riscv)') python_module_regex = re.compile(r'.*\.\w*(python|pypy)\w*(-\w+){4}\.so') lto_text_like_sections = {'.preinit_array', '.init_array', '.fini_array'} # The following sections are part of the RX ABI and do correspond to .text, .data and .bss lto_text_like_sections |= {'P', 'D_1', 'B_1'} # The list is taken from glibc: sysdeps/${arch}/stackinfo.h default_executable_stack_archs = re.compile(r'alpha|arm.*|hppa|i.86|m68k|microblaze|mips|ppc|s390|s390x|sh|sparc|x86_64') rpath_origin = '$ORIGIN' hpc_locations = ('/usr/lib/mpi/', '/usr/lib64/mpi/', '/usr/lib/hpc/') def __init__(self, config, output): super().__init__(config, output) self.checked_files = 0 self.system_lib_paths = tuple(config.configuration['SystemLibPaths']) self.pie_exec_regex_list = [] for regex in config.configuration['PieExecutables']: self.pie_exec_regex_list.append(re.compile(regex)) self.usr_lib_exception_regex = re.compile(config.configuration['UsrLibBinaryException']) self.setgid_call_regex = self.create_regexp_call(r'set(?:res|e)?gid') self.setuid_call_regex = self.create_regexp_call(r'set(?:res|e)?uid') self.setgroups_call_regex = self.create_regexp_call(r'(?:ini|se)tgroups') self.mktemp_call_regex = self.create_regexp_call('mktemp') self.gethostbyname_call_regex = self.create_regexp_call(r'(gethostbyname|gethostbyname2|gethostbyaddr|gethostbyname_r|gethostbyname2_r|gethostbyaddr_r)') # register all check functions self.check_functions = [self._check_lto_section, self._check_no_text_in_archive, self._check_missing_symtab_in_archive, self._check_missing_debug_info_in_archive, self._check_executable_stack, self._check_shared_library, self._check_dependency, self._check_library_dependency_location, self._check_security_functions, self._check_rpath, self._check_library_dependency, self._check_forbidden_functions, self._check_executable_shlib, self._check_optflags] @staticmethod def create_nonlibc_regexp_call(call): r = r'(%s)\s?.*$' % call return re.compile(r) @staticmethod def create_regexp_call(call): r = r'(%s(?:@GLIBC\S+)?)(?:\s|$)' % call return re.compile(r) def _check_libtool_wrapper(self, pkg, fname, pkgfile): """ Print an error if the fname file contains a libtool wrapper shell script. """ if 'shell script' in pkgfile.magic: file_start = None with contextlib.suppress(IOError), open(pkgfile.path, 'rb') as inputf: file_start = inputf.read(2048) if (file_start and b'This wrapper script should never ' b'be moved out of the build directory' in file_start): self.output.add_info('E', pkg, 'libtool-wrapper-in-package', fname) def _check_invalid_la_file(self, pkg, fname): """ Check if the fname is an .la file and contains a reference to the invalid directories ('/tmp' or '/home'). If so then print a corresponding error with the matching line numbers. """ if self.la_file_regex.search(fname): line = pkg.grep(self.invalid_dir_ref_regex, fname) if line: self.output.add_info('E', pkg, 'invalid-la-file', fname, f'(line {line})') def _check_binary_in_noarch(self, pkg, bin_name): """ Print an error if the binary file bin_name is in the noarch package. """ if pkg.arch == 'noarch': self.output.add_info('E', pkg, 'arch-independent-package-contains-binary-or-object', bin_name) def _check_binary_in_usr_share(self, pkg, bin_name): """ Print an error if binary file bin_name is installed in /usr/share. We suppose that the package is arch dependent. """ if bin_name.startswith('/usr/share/') and \ not self.usr_arch_share_regex.search(bin_name): self.output.add_info('E', pkg, 'arch-dependent-file-in-usr-share', bin_name) def _check_binary_in_etc(self, pkg, bin_name): """ Print an error if binary file bin_name is installed in /etc directory. We suppose that the package is arch dependent. """ if bin_name.startswith('/etc/'): self.output.add_info('E', pkg, 'binary-in-etc', bin_name) def _check_unstripped_binary(self, bin_name, pkg, pkgfile): """ Print a warning if the bin_name binary has unstripped debug symbols. We suppose that the package is arch dependent and bin_name is not ocaml native, lua bytecode, .o or .static. """ if 'not stripped' in pkgfile.magic: self.output.add_info('W', pkg, 'unstripped-binary-or-object', bin_name) def _check_non_pie(self, pkg, bin_name): """ Check if the bin_name binary is built with PIE. Print an error message if it's not while PIE is forced in configuration. Print a warning if it's not forced. We suppose that the package is arch dependent and bin_name is binary executable. """ if not self.is_shobj and not self.is_pie_exec: if any(regex.fullmatch(bin_name) for regex in self.pie_exec_regex_list): self.output.add_info('E', pkg, 'non-position-independent-executable', bin_name) else: self.output.add_info('W', pkg, 'position-independent-executable-suggested', bin_name) def _check_exec_in_library(self, pkg, has_lib, exec_files): """ Check if the library package has an executable file installed. Print an error for every such file. """ if has_lib: for f in exec_files: self.output.add_info('E', pkg, 'executable-in-library-package', f) def _check_non_versioned(self, pkg, has_lib, exec_files): """ Check if the library package contains library files in non-versioned directories. Print an error for every such file. """ if has_lib: for f in pkg.files: res = self.numeric_dir_regex.search(f) fn = res and res.group(1) or f if f not in exec_files and not self.so_regex.search(f) and \ not self.versioned_dir_regex.search(fn): self.output.add_info('E', pkg, 'non-versioned-file-in-library-package', f) def _check_no_binary(self, pkg, has_binary, has_file_in_lib64): """ Check if the arch dependent package contains any binaries. Print an error if there is no binary and it's not noarch. """ if not has_binary and not has_file_in_lib64 and \ pkg.arch != 'noarch': self.output.add_info('E', pkg, 'no-binary') def _check_noarch_with_lib64(self, pkg, has_file_in_lib64): """ Print an error if we have a noarch package that contains a file in /usr/lib64. """ if pkg.arch == 'noarch' and has_file_in_lib64: self.output.add_info('E', pkg, 'noarch-with-lib64') def _check_only_non_binary_in_usrlib(self, pkg, has_usr_lib_file, has_binary_in_usr_lib): """ Check and print a warning if we have _only_ non-binary files in the '/usr/lib'. Note: non-binaries allowed via UsrLibBinaryException config option are considered binaries. """ if has_usr_lib_file and not has_binary_in_usr_lib: self.output.add_info('W', pkg, 'only-non-binary-in-usr-lib') def _check_no_text_in_archive(self, pkg, pkgfile): """ For an archive, test if any .text sections is non-empty. """ if self.is_archive: for comment in self.readelf_parser.comment_section_info.comments: if comment.startswith('GHC '): return # Starting with glibc 2.34, some static libraries were moved to libc # and there are empty archives for backward compatibility. Skip these # libraries. stem = Path(pkgfile.name).stem if stem in GLIBC_EMPTY_ARCHIVES or (stem.endswith('_p') and stem[:-2] in GLIBC_EMPTY_ARCHIVES): return for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: sn = section.name if ((sn in self.lto_text_like_sections or sn == '.fini_array' or sn.startswith('.text') or sn.startswith('.data')) and section.size > 0): return self.output.add_info('E', pkg, 'lto-no-text-in-archive', pkgfile.name) return def _check_missing_symtab_in_archive(self, pkg, pkgfile): """ FIXME Add test coverage. """ if self.is_archive: for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: if section.name == '.symtab': return self.output.add_info('E', pkg, 'static-library-without-symtab', pkgfile.name) def _check_missing_debug_info_in_archive(self, pkg, pkgfile): if self.is_archive: for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: if section.name.startswith('.debug_'): return self.output.add_info('E', pkg, 'static-library-without-debuginfo', pkgfile.name) # Check for LTO sections def _check_lto_section(self, pkg, pkgfile): for elf_file in self.readelf_parser.section_info.elf_files: for section in elf_file: if '.gnu.lto_.' in section.name: self.output.add_info('E', pkg, 'lto-bytecode', pkgfile.name) return def _check_executable_stack(self, pkg, pkgfile): """ Check if the stack is declared as executable which is usually an error. """ # Skip architectures that have non-executable stack by default if pkg.arch and not self.default_executable_stack_archs.fullmatch(pkg.arch): return # Do not check kernel modules and archives if not self.is_archive and not any(pkgfile.name.startswith(p) for p in KERNEL_MODULES_PATHS): stack_headers = [h for h in self.readelf_parser.program_header_info.headers if h.name == 'GNU_STACK'] if not stack_headers: self.output.add_info('E', pkg, 'missing-PT_GNU_STACK-section', pkgfile.name) elif 'E' in stack_headers[0].flags: self.output.add_info('E', pkg, 'executable-stack', pkgfile.name) def _check_soname_symlink(self, pkg, shlib, soname): """ Check that we have a symlink with the soname in the package and it points to the checked shared library. Print an error if the symlink is invalid or missing. """ path = Path(shlib) symlink = path.parent / soname try: link = pkg.files[str(symlink)].linkto if link not in (shlib, path.name, ''): self.output.add_info('E', pkg, 'invalid-ldconfig-symlink', shlib, link) except KeyError: # if we do not have a symlink, report an issue if path.name.startswith('lib') or path.name.startswith('ld-'): self.output.add_info('E', pkg, 'no-ldconfig-symlink', shlib) def _check_shared_library(self, pkg, pkgfile): """ Various checks for the shared library. 1) Print 'no-soname' warning it the library has no soname present. 2) Print 'invalid-soname' error if the soname is not valid. 3) Print 'shlib-policy-name-error' error when the library major version is not present in the package name. 4) Print 'shlib-with-non-pic-code' error if the library contains object code that was compiled without -fPIC. """ if not self.readelf_parser.is_shlib: return soname = self.readelf_parser.dynamic_section_info.soname if not soname: self.output.add_info('W', pkg, 'no-soname', pkgfile.name) else: if not self.validso_regex.search(soname): self.output.add_info('E', pkg, 'invalid-soname', pkgfile.name, soname) else: self._check_soname_symlink(pkg, pkgfile.name, soname) # check if the major version of the library is in the package # name (check only for lib* packages) # for now skip all HPC libraries (#901) if pkg.name.startswith('lib') and not pkgfile.name.startswith(self.hpc_locations): # SLPP is defined here: https://en.opensuse.org/openSUSE:Shared_library_packaging_policy#Package_naming # Example: # SONAME = libgame2-1.9.so.10.0.0 # expected package name: libgame2-1_9-10_0_0 res = self.soversion_regex.search(soname) if res: parts = [x.replace('.', '_') for x in (res.group('pkgversion'), res.group('soversion')) if x] soversion = '-'.join(parts) pkgname = pkg.name if '.' in pkgname: pkgname = pkgname[:pkgname.rindex('.')] if soversion and not pkgname.endswith(soversion): self.output.add_info('E', pkg, 'shlib-policy-name-error', f'SONAME: {soname} ({pkgfile.name}), expected package suffix: {soversion}') # check if the object code in the library is compiled with PIC if self.readelf_parser.dynamic_section_info['TEXTREL']: self.output.add_info('E', pkg, 'shlib-with-non-pic-code', pkgfile.name) def _check_dependency(self, pkg, pkgfile): """ FIXME Add test coverage. """ # Undefined symbol and unused direct dependency checks make sense only # for installed packages. # skip debuginfo: https://bugzilla.redhat.com/190599 # # following issues are errors for shared libs and warnings for executables if not self.is_dynamically_linked: return # Skip python packages if self.python_module_regex.fullmatch(pkgfile.name): return if not self.is_archive and not self.readelf_parser.is_debug: info_type = 'E' if self.readelf_parser.is_shlib else 'W' for symbol in self.ldd_parser.undefined_symbols: self.output.add_info(info_type, pkg, 'undefined-non-weak-symbol', pkgfile.name, symbol) for dependency in self.ldd_parser.unused_dependencies: self.output.add_info(info_type, pkg, 'unused-direct-shlib-dependency', pkgfile.name, dependency) def _check_library_dependency_location(self, pkg, pkgfile): """ FIXME Add test coverage. """ if not self.is_dynamically_linked: return if not self.is_archive: for dependency in self.ldd_parser.dependencies: if dependency.startswith('/opt/'): self.output.add_info('E', pkg, 'linked-against-opt-library', pkgfile.name, dependency) break nonusr = ('/bin', '/lib', '/sbin') if pkgfile.name.startswith(nonusr): for dependency in self.ldd_parser.dependencies: if dependency.startswith('/usr/'): self.output.add_info('W', pkg, 'linked-against-usr-library', pkgfile.name, dependency) break def _check_security_functions(self, pkg, pkgfile): setgid = any(self.readelf_parser.symbol_table_info.get_functions_for_regex(self.setgid_call_regex)) setuid = any(self.readelf_parser.symbol_table_info.get_functions_for_regex(self.setuid_call_regex)) setgroups = any(self.readelf_parser.symbol_table_info.get_functions_for_regex(self.setgroups_call_regex)) mktemp = any(self.readelf_parser.symbol_table_info.get_functions_for_regex(self.mktemp_call_regex)) gethostbyname = any(self.readelf_parser.symbol_table_info.get_functions_for_regex(self.gethostbyname_call_regex)) if setgid and setuid and not setgroups: is_uid = stat.S_ISUID & pkgfile.mode self.output.add_info('W' if is_uid else 'E', pkg, 'missing-call-to-setgroups-before-setuid', pkgfile.name) if mktemp: self.output.add_info('E', pkg, 'call-to-mktemp', pkgfile.name) if gethostbyname: self.output.add_info('W', pkg, 'binary-or-shlib-calls-gethostbyname', pkgfile.name) def _check_rpath(self, pkg, pkgfile): for runpaths in self.readelf_parser.dynamic_section_info.runpaths: for runpath in runpaths.split(':'): if self.rpath_origin in runpath: runpath = runpath.replace(self.rpath_origin, str(Path(pkgfile.name).parent)) runpath = str(Path(runpath).resolve()) if not runpath.startswith(self.system_lib_paths) and not self.usr_lib_regex.search(runpath): self.output.add_info('E', pkg, 'binary-or-shlib-defines-rpath', pkgfile.name, f'(RUNPATH: {runpath})') return def _check_library_dependency(self, pkg, pkgfile): if (self.is_archive or any(pkgfile.name.startswith(p) for p in KERNEL_MODULES_PATHS) or self.python_module_regex.fullmatch(pkgfile.name)): return dyn_section = self.readelf_parser.dynamic_section_info if not len(dyn_section.needed) and not (dyn_section.soname and self.ldso_soname_regex.search(dyn_section.soname)): if self.is_shobj: msg = 'shared-library-without-dependency-information' else: msg = 'statically-linked-binary' self.output.add_info('E', pkg, msg, pkgfile.name) else: # linked against libc ? if 'libc.' not in dyn_section.runpaths and \ (not dyn_section.soname or ('libc.' not in dyn_section.soname and not self.ldso_soname_regex.search(dyn_section.soname))): for lib in dyn_section.needed: if 'libc.' in lib: return if self.is_shobj: msg = 'library-not-linked-against-libc' else: msg = 'program-not-linked-against-libc' self.output.add_info('W', pkg, msg, pkgfile.name) def _check_forbidden_functions(self, pkg, pkgfile): forbidden_functions = self.config.configuration['WarnOnFunction'] if forbidden_functions: for name, func in forbidden_functions.items(): # precompile regexps f_name = func['f_name'] func['f_regex'] = self.create_nonlibc_regexp_call(f_name) if 'good_param' in func and func['good_param']: func['waiver_regex'] = re.compile(func['good_param']) # register descriptions self.output.error_details.update({name: func['description']}) forbidden_calls = [] for r_name, func in forbidden_functions.items(): if any(self.readelf_parser.symbol_table_info.get_functions_for_regex(func['f_regex'])): forbidden_calls.append(r_name) if not forbidden_calls: return strings_parser = StringsParser(pkgfile.path) failed_reason = strings_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'strings-failed', pkgfile.name, failed_reason) return forbidden_functions_filtered = [] for fn in forbidden_calls: f = forbidden_functions[fn] if 'waiver_regex' not in f: forbidden_functions_filtered.append(fn) continue waiver = any(map(lambda string: f['waiver_regex'].search(string), strings_parser.strings)) if not waiver: forbidden_functions_filtered.append(fn) for fn in forbidden_functions_filtered: self.output.add_info('W', pkg, fn, pkgfile.name, forbidden_functions[fn]['f_name']) def _check_executable_shlib(self, pkg, pkgfile): if not (pkgfile.mode & stat.S_IEXEC) and self.readelf_parser.is_shlib: self.output.add_info('E', pkg, 'shared-library-not-executable', pkgfile.name) def _check_optflags(self, pkg, pkgfile): if self.is_archive: return mandatory_optflags = self.config.configuration['MandatoryOptflags'] forbidden_optflags = self.config.configuration['ForbiddenOptflags'] if not mandatory_optflags and not forbidden_optflags: return for dwarf_unit in self.objdump_parser.compile_units: tokens = dwarf_unit['producer'].split(' ') missing = [mo for mo in mandatory_optflags if mo not in tokens] forbidden = [f for f in forbidden_optflags if f in tokens] if missing: self.output.add_info('W', pkg, 'missing-mandatory-optflags', pkgfile.name, ' '.join(missing)) if forbidden: self.output.add_info('E', pkg, 'forbidden-optflags', pkgfile.name, ' '.join(forbidden)) def _is_standard_archive(self, pkg, pkgfile): # skip Klee bytecode archives if pkgfile.path.endswith('.bca'): return False # return false for e.g. Rust or Go packages that are archives # but files in the archive are not an ELF container ar_parser = ArParser(pkgfile.path) failed_reason = ar_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'ar-failed', pkgfile.name, failed_reason) return False needles = ('__.PKGDEF', '_go_.o', 'lib.rmeta') return not any(needle for needle in needles if needle in ar_parser.objects) def _detect_attributes(self, magic): self.is_exec = 'executable' in magic self.is_shobj = 'shared object' in magic self.is_archive = 'current ar archive' in magic self.is_dynamically_linked = 'dynamically linked' in magic self.is_pie_exec = 'pie executable' in magic self.is_nonstandard_archive = False def run_elf_checks(self, pkg, pkgfile): if self.is_archive and not self._is_standard_archive(pkg, pkgfile): self.is_nonstandard_archive = True return self.readelf_parser = ReadelfParser(pkgfile.path, pkgfile.name) failed_reason = self.readelf_parser.parsing_failed_reason() if failed_reason: self.output.add_info('E', pkg, 'readelf-failed', pkgfile.name, failed_reason) return if not self.is_archive: if self.is_dynamically_linked: is_installed_pkg = isinstance(pkg, (InstalledPkg, FakePkg)) self.ldd_parser = LddParser(pkgfile.path, pkgfile.name, is_installed_pkg) failed_reason = self.ldd_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'ldd-failed', pkgfile.name, failed_reason) return if (self.config.configuration['MandatoryOptflags'] or self.config.configuration['ForbiddenOptflags']): self.objdump_parser = ObjdumpParser(pkgfile.path, pkgfile.name) failed_reason = self.objdump_parser.parsing_failed_reason if failed_reason: self.output.add_info('E', pkg, 'objdump-failed', pkgfile.name, failed_reason) return # NOTE: the speed benefit of the ThreadPoolExecutor is limited due to # Global Interpreter Lock (GIL). with concurrent.futures.ThreadPoolExecutor() as executor: futures = [] for fn in self.check_functions: futures.append(executor.submit(fn, pkg, pkgfile)) concurrent.futures.wait(futures) for future in futures: err = future.exception() if err: raise err def check_binary(self, pkg): exec_files = [] pkg_has_lib = False pkg_has_binary = False pkg_has_binary_in_usrlib = False pkg_has_usrlib_file = False pkg_has_file_in_lib64 = False # go through the all files, run files checks and collect data that are # needed later for fname, pkgfile in pkg.files.items(): # Common tests first self._check_libtool_wrapper(pkg, fname, pkgfile) self._check_invalid_la_file(pkg, fname) # consider non-binary in /usr/lib/ that is allowed by # UsrLibBinaryException config option as a "fake" binary and # do not throw 'only-non-binary-in-usr-lib' warning then if not stat.S_ISDIR(pkgfile.mode) and self.usr_lib_regex.search(fname): pkg_has_usrlib_file = True if not pkg_has_binary_in_usrlib and \ self.usr_lib_exception_regex.search(fname): # Fake that we have binaries there to avoid # only-non-binary-in-usr-lib false positives pkg_has_binary_in_usrlib = True # find out if we have a file in /usr/lib64/ directory (needed later # for the package checks) if stat.S_ISREG(pkgfile.mode) and \ (fname.startswith('/usr/lib64') or fname.startswith('/lib64')): pkg_has_file_in_lib64 = True # skip the rest of the tests for non-binaries # binary files only from here on is_ocaml_native = 'Objective caml native' in pkgfile.magic is_lua_bytecode = 'Lua bytecode' in pkgfile.magic if not (pkgfile.magic.startswith('ELF ') or 'current ar archive' in pkgfile.magic or is_ocaml_native or is_lua_bytecode): continue self.checked_files += 1 # mark this package as a one that has binary file pkg_has_binary = True # if there is a binary in /usr/lib then mark this package # accordingly if pkg_has_usrlib_file and not pkg_has_binary_in_usrlib and \ self.usr_lib_regex.search(fname): pkg_has_binary_in_usrlib = True self._check_binary_in_noarch(pkg, fname) # skip the rest of the tests for noarch packages # arch dependent packages only from here on if pkg.arch == 'noarch': continue self._check_binary_in_usr_share(pkg, fname) self._check_binary_in_etc(pkg, fname) # skip the rest of the tests for ocaml native, Lua bytecode, # Go .go and .gox, .o and .static if is_ocaml_native or is_lua_bytecode or fname.endswith('.o') or \ fname.endswith('.static') or fname.endswith('.gox') or \ fname.endswith('.go'): continue self._check_unstripped_binary(fname, pkg, pkgfile) # Detect attributes of an ELF file self._detect_attributes(pkgfile.magic) # run ELF checks self.run_elf_checks(pkg, pkgfile) if self.is_nonstandard_archive: continue # inspect binary file is_shlib = self.readelf_parser.is_shlib if is_shlib: pkg_has_lib = True # skip non-exec and non-SO # executables and shared objects only from here on if not self.is_exec and not self.is_shobj: continue if self.is_shobj and not self.is_exec and '.so' not in fname and \ self.bin_regex.search(fname): # pkgfile.magic does not contain 'executable' for PIEs self.is_exec = True if self.is_exec: # add to the list of the all exec files if self.bin_regex.search(fname): exec_files.append(fname) self._check_non_pie(pkg, fname) # run checks for the whole package # it uses data collected in the previous for-cycle self._check_exec_in_library(pkg, pkg_has_lib, exec_files) self._check_non_versioned(pkg, pkg_has_lib, exec_files) self._check_no_binary(pkg, pkg_has_binary, pkg_has_file_in_lib64) self._check_noarch_with_lib64(pkg, pkg_has_file_in_lib64) self._check_only_non_binary_in_usrlib(pkg, pkg_has_usrlib_file, pkg_has_binary_in_usrlib)
def check(self, pkg): if pkg.is_source: return # Consider only non-development, non-language library packages if (not pkg.name.startswith('lib') or pkg.name.endswith('-devel') or pkg.name.endswith('-lang')): return self._check_missing_policy_lib(pkg) # the soname validation matching the name is done # already in BinaryCheck._check_shared_library # Search for shared libraries in this package libs = set() libs_needed = set() libs_to_dir = {} reqlibs = set() pkg_requires = {x.name.split('(')[0] for x in pkg.requires} for filename, pkgfile in pkg.files.items(): path = Path(filename) if '.so.' in filename or filename.endswith('.so'): if stat.S_ISREG(pkg.files[filename].mode ) and pkgfile.magic.startswith('ELF '): readelf_parser = ReadelfParser(pkgfile.path, filename) failed_reason = readelf_parser.parsing_failed_reason() if failed_reason: self.output.add_info('E', pkg, 'readelf-failed', filename, failed_reason) return dyn_section = readelf_parser.dynamic_section_info libs_needed = libs_needed.union(dyn_section.needed) if dyn_section.soname: lib_dir = str(path.parent) libs.add(dyn_section.soname) libs_to_dir[dyn_section.soname] = lib_dir if dyn_section.soname in pkg_requires: # But not if the library is used by the pkg itself # This avoids program packages with their own # private lib # FIXME: we'd need to check if somebody else links # to this lib reqlibs.add(dyn_section.soname) if not libs.difference(reqlibs): return if pkg.name[-1].isdigit(): # ignore libs in a versioned non_std_dir for lib in libs.copy(): lib_dir = libs_to_dir[lib] for lib_part in lib_dir.split('/'): if not lib_part: continue if lib_part[-1].isdigit( ) and not lib_part.endswith('lib64'): libs.remove(lib) break # Check for non-versioned libs in a std lib package for lib in libs.copy(): if (not (lib[-1].isdigit() or self.re_soname_strongly_versioned.search(lib))): self.output.add_info('W', pkg, 'shlib-unversioned-lib', lib) # Verify shared lib policy package doesn't have hard dependency on non-lib packages for dep in pkg.requires: if dep[0].startswith('rpmlib(') or dep[0].startswith( 'config('): continue if (dep[1] & (rpm.RPMSENSE_GREATER | rpm.RPMSENSE_EQUAL)) == rpm.RPMSENSE_EQUAL: self.output.add_info('W', pkg, 'shlib-fixed-dependency', formatRequire(dep[0], dep[1], dep[2])) # Verify non-lib stuff does not add dependencies if libs: for dep in pkg_requires: if '.so.' in dep and dep not in libs and dep not in libs_needed: self.output.add_info('E', pkg, 'shlib-policy-excessive-dependency', dep)