523 lines
22 KiB
Python
523 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
#############################################################################
|
|
# File : BinariesCheck.py
|
|
# Package : rpmlint
|
|
# Author : Frederic Lepied
|
|
# Created on : Tue Sep 28 07:01:42 1999
|
|
# Version : $Id$
|
|
# Purpose : check binary files in a binary rpm package.
|
|
#############################################################################
|
|
|
|
import os
|
|
import re
|
|
import stat
|
|
|
|
import rpm
|
|
|
|
from Filter import printError, printWarning
|
|
import AbstractCheck
|
|
import Config
|
|
import Pkg
|
|
|
|
|
|
DEFAULT_SYSTEM_LIB_PATHS = (
|
|
'/lib', '/usr/lib', '/usr/X11R6/lib',
|
|
'/lib64', '/usr/lib64', '/usr/X11R6/lib64')
|
|
|
|
class BinaryInfo:
|
|
|
|
needed_regex = re.compile('\s+\(NEEDED\).*\[(\S+)\]')
|
|
rpath_regex = re.compile('\s+\(RPATH\).*\[(\S+)\]')
|
|
soname_regex = re.compile('\s+\(SONAME\).*\[(\S+)\]')
|
|
comment_regex = re.compile('^\s+\[\s*\d+\]\s+\.comment\s+')
|
|
pic_regex = re.compile('^\s+\[\s*\d+\]\s+\.rela?\.(data|text)')
|
|
# GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
|
|
stack_regex = re.compile('^\s+GNU_STACK\s+(?:(?:\S+\s+){5}(\S+)\s+)?')
|
|
stack_exec_regex = re.compile('^..E$')
|
|
non_pic_regex = re.compile('TEXTREL', re.MULTILINE)
|
|
undef_regex = re.compile('^undefined symbol:\s+(\S+)')
|
|
unused_regex = re.compile('^\s+(\S+)')
|
|
debug_file_regex = re.compile('\.debug$')
|
|
exit_call_regex = re.compile('\s+FUNC\s+.*?\s+(_?exit(?:@\S+)?)(?:\s|$)')
|
|
fork_call_regex = re.compile('\s+FUNC\s+.*?\s+(fork(?:@\S+)?)(?:\s|$)')
|
|
|
|
def __init__(self, pkg, path, file, is_ar, is_shlib):
|
|
self.readelf_error = 0
|
|
self.needed = []
|
|
self.rpath = []
|
|
self.undef = []
|
|
self.unused = []
|
|
self.comment = 0
|
|
self.soname = 0
|
|
self.non_pic = 1
|
|
self.stack = 0
|
|
self.exec_stack = 0
|
|
self.exit_calls = []
|
|
fork_called = 0
|
|
self.tail = ''
|
|
|
|
is_debug = BinaryInfo.debug_file_regex.search(path)
|
|
|
|
cmd = ['env', 'LC_ALL=C', 'readelf', '-W', '-S', '-l', '-d', '-s']
|
|
cmd.append(path)
|
|
res = Pkg.getstatusoutput(cmd)
|
|
if not res[0]:
|
|
for l in res[1].splitlines():
|
|
|
|
r = BinaryInfo.needed_regex.search(l)
|
|
if r:
|
|
self.needed.append(r.group(1))
|
|
continue
|
|
|
|
r = BinaryInfo.rpath_regex.search(l)
|
|
if r:
|
|
for p in r.group(1).split(':'):
|
|
self.rpath.append(p)
|
|
continue
|
|
|
|
if BinaryInfo.comment_regex.search(l):
|
|
self.comment = 1
|
|
continue
|
|
|
|
if BinaryInfo.pic_regex.search(l):
|
|
self.non_pic = 0
|
|
continue
|
|
|
|
r = BinaryInfo.soname_regex.search(l)
|
|
if r:
|
|
self.soname = r.group(1)
|
|
continue
|
|
|
|
r = BinaryInfo.stack_regex.search(l)
|
|
if r:
|
|
self.stack = 1
|
|
flags = r.group(1)
|
|
if flags and BinaryInfo.stack_exec_regex.search(flags):
|
|
self.exec_stack = 1
|
|
continue
|
|
|
|
if is_shlib:
|
|
r = BinaryInfo.exit_call_regex.search(l)
|
|
if r:
|
|
self.exit_calls.append(r.group(1))
|
|
continue
|
|
r = BinaryInfo.fork_call_regex.search(l)
|
|
if r:
|
|
fork_called = 1
|
|
continue
|
|
|
|
if self.non_pic:
|
|
self.non_pic = BinaryInfo.non_pic_regex.search(res[1])
|
|
|
|
# Ignore all exit() calls if fork() is being called.
|
|
# Does not have any context at all but without this kludge, the
|
|
# number of false positives would probably be intolerable.
|
|
if fork_called:
|
|
self.exit_calls = []
|
|
|
|
else:
|
|
self.readelf_error = 1
|
|
printWarning(pkg, 'binaryinfo-readelf-failed',
|
|
file, re.sub('\n.*', '', res[1]))
|
|
|
|
fobj = None
|
|
try:
|
|
fobj = open(path)
|
|
fobj.seek(-12, os.SEEK_END)
|
|
self.tail = fobj.read()
|
|
except Exception, e:
|
|
printWarning(pkg, 'binaryinfo-tail-failed %s: %s' % (file, e))
|
|
if fobj:
|
|
fobj.close()
|
|
|
|
# Undefined symbol and unused direct dependency checks make sense only
|
|
# for installed packages.
|
|
# skip debuginfo: https://bugzilla.redhat.com/190599
|
|
if not is_ar and not is_debug and isinstance(pkg, Pkg.InstalledPkg):
|
|
# We could do this with objdump, but it's _much_ simpler with ldd.
|
|
res = Pkg.getstatusoutput(
|
|
('env', 'LC_ALL=C', 'ldd', '-d', '-r', path))
|
|
if not res[0]:
|
|
for l in res[1].splitlines():
|
|
undef = BinaryInfo.undef_regex.search(l)
|
|
if undef:
|
|
self.undef.append(undef.group(1))
|
|
else:
|
|
printWarning(pkg, 'ldd-failed', file)
|
|
res = Pkg.getstatusoutput(
|
|
('env', 'LC_ALL=C', 'ldd', '-r', '-u', path))
|
|
if res[0]:
|
|
# Either ldd doesn't grok -u (added in glibc 2.3.4) or we have
|
|
# unused direct dependencies
|
|
in_unused = 0
|
|
for l in res[1].splitlines():
|
|
if not l.rstrip():
|
|
pass
|
|
elif l.startswith('Unused direct dependencies'):
|
|
in_unused = 1
|
|
elif in_unused:
|
|
unused = BinaryInfo.unused_regex.search(l)
|
|
if unused:
|
|
self.unused.append(unused.group(1))
|
|
else:
|
|
in_unused = 0
|
|
|
|
path_regex = re.compile('(.*/)([^/]+)')
|
|
numeric_dir_regex = re.compile('/usr(?:/share)/man/man./(.*)\.[0-9](?:\.gz|\.bz2)')
|
|
versioned_dir_regex = re.compile('[^.][0-9]')
|
|
usr_share = re.compile('^/usr/share/')
|
|
etc = re.compile('^/etc/')
|
|
not_stripped = re.compile('not stripped')
|
|
unstrippable = re.compile('\.o$|\.static$')
|
|
shared_object_regex = re.compile('shared object')
|
|
executable_regex = re.compile('executable')
|
|
libc_regex = re.compile('libc\.')
|
|
ldso_soname_regex = re.compile('^ld(-linux(-(ia|x86_)64))?\.so')
|
|
so_regex = re.compile('/lib(64)?/[^/]+\.so(\.[0-9]+)*$')
|
|
validso_regex = re.compile('(\.so\.\d+(\.\d+)*|\d\.so)$')
|
|
sparc_regex = re.compile('SPARC32PLUS|SPARC V9|UltraSPARC')
|
|
system_lib_paths = Config.getOption('SystemLibPaths', DEFAULT_SYSTEM_LIB_PATHS)
|
|
usr_lib_regex = re.compile('^/usr/lib(64)?/')
|
|
bin_regex = re.compile('^(/usr(/X11R6)?)?/s?bin/')
|
|
soversion_regex = re.compile('.*?([0-9][.0-9]*)\\.so|.*\\.so\\.([0-9][.0-9]*).*')
|
|
reference_regex = re.compile('\.la$|^/usr/lib(64)?/pkgconfig/')
|
|
usr_lib_exception_regex = re.compile(Config.getOption('UsrLibBinaryException', '^/usr/lib(64)?/(perl|python|ruby|menu|pkgconfig|ocaml|lib[^/]+\.(so|l?a)$|bonobo/servers/)'))
|
|
srcname_regex = re.compile('(.*?)-[0-9]')
|
|
invalid_dir_ref_regex = re.compile('/(home|tmp)(\W|$)')
|
|
ocaml_mixed_regex = re.compile('^Caml1999X0\d\d$')
|
|
|
|
def dir_base(path):
|
|
res = path_regex.search(path)
|
|
if res:
|
|
return res.group(1), res.group(2)
|
|
else:
|
|
return '', path
|
|
|
|
class BinariesCheck(AbstractCheck.AbstractCheck):
|
|
|
|
def __init__(self):
|
|
AbstractCheck.AbstractCheck.__init__(self, 'BinariesCheck')
|
|
|
|
def check(self, pkg):
|
|
# Check only binary package
|
|
if pkg.isSource():
|
|
return
|
|
|
|
info = pkg.getFilesInfo()
|
|
files = pkg.files()
|
|
exec_files = []
|
|
has_lib = []
|
|
version = None
|
|
binary = 0
|
|
binary_in_usr_lib = 0
|
|
has_usr_lib_file = 0
|
|
|
|
res = srcname_regex.search(pkg[rpm.RPMTAG_SOURCERPM] or '')
|
|
if res:
|
|
multi_pkg = (pkg.name != res.group(1))
|
|
else:
|
|
multi_pkg = 0
|
|
|
|
for f in files:
|
|
if usr_lib_regex.search(f) and not usr_lib_exception_regex.search(f) and not stat.S_ISDIR(files[f][0]):
|
|
has_usr_lib_file = f
|
|
break
|
|
|
|
for i in info:
|
|
is_elf = i[1].find('ELF') != -1
|
|
is_ar = i[1].find('current ar archive') != -1
|
|
is_ocaml_native = i[1].find('Objective caml native') != -1
|
|
is_binary = is_elf or is_ar or is_ocaml_native
|
|
is_shlib = so_regex.search(i[0])
|
|
|
|
if is_binary:
|
|
binary += 1
|
|
if has_usr_lib_file and not binary_in_usr_lib and usr_lib_regex.search(i[0]):
|
|
binary_in_usr_lib = 1
|
|
|
|
if pkg.arch == 'noarch':
|
|
printError(pkg, 'arch-independent-package-contains-binary-or-object', i[0])
|
|
else:
|
|
# in /usr/share ?
|
|
if usr_share.search(i[0]):
|
|
printError(pkg, 'arch-dependent-file-in-usr-share', i[0])
|
|
# in /etc ?
|
|
if etc.search(i[0]):
|
|
printError(pkg, 'binary-in-etc', i[0])
|
|
|
|
if pkg.arch == 'sparc' and sparc_regex.search(i[1]):
|
|
printError(pkg, 'non-sparc32-binary', i[0])
|
|
|
|
# stripped ?
|
|
if not unstrippable.search(i[0]) and not is_ocaml_native:
|
|
if not_stripped.search(i[1]):
|
|
printWarning(pkg, 'unstripped-binary-or-object', i[0])
|
|
|
|
# inspect binary file
|
|
bin_info = BinaryInfo(pkg, pkg.dirName()+i[0], i[0], is_ar, is_shlib)
|
|
|
|
# so name in library
|
|
if is_shlib:
|
|
has_lib.append(i[0])
|
|
if bin_info.readelf_error:
|
|
pass
|
|
elif not bin_info.soname:
|
|
printWarning(pkg, 'no-soname', i[0])
|
|
else:
|
|
if not validso_regex.search(bin_info.soname):
|
|
printError(pkg, 'invalid-soname', i[0], bin_info.soname)
|
|
else:
|
|
(directory, base) = dir_base(i[0])
|
|
try:
|
|
symlink = directory + bin_info.soname
|
|
(perm, owner, group, link, size, md5, mtime, rdev) = files[symlink]
|
|
if link != i[0] and link != base and link != '':
|
|
printError(pkg, 'invalid-ldconfig-symlink', i[0], link)
|
|
except KeyError:
|
|
printError(pkg, 'no-ldconfig-symlink', i[0])
|
|
res = soversion_regex.search(bin_info.soname)
|
|
if res:
|
|
soversion = res.group(1) or res.group(2)
|
|
if version == None:
|
|
version = soversion
|
|
elif version != soversion:
|
|
version = -1
|
|
|
|
if bin_info.non_pic and not bin_info.readelf_error:
|
|
printError(pkg, 'shlib-with-non-pic-code', i[0])
|
|
|
|
# rpath ?
|
|
if bin_info.rpath:
|
|
for p in bin_info.rpath:
|
|
if p in system_lib_paths or \
|
|
not usr_lib_regex.search(p):
|
|
printError(pkg, 'binary-or-shlib-defines-rpath', i[0], bin_info.rpath)
|
|
break
|
|
|
|
# shared lib calls exit() or _exit()?
|
|
if is_shlib and bin_info.exit_calls:
|
|
for ec in bin_info.exit_calls:
|
|
printWarning(pkg, 'shared-lib-calls-exit', i[0], ec)
|
|
|
|
is_exec = executable_regex.search(i[1])
|
|
if shared_object_regex.search(i[1]) or \
|
|
is_exec:
|
|
|
|
if is_exec:
|
|
|
|
if bin_regex.search(i[0]):
|
|
exec_files.append(i[0])
|
|
|
|
if ocaml_mixed_regex.search(bin_info.tail):
|
|
printWarning(pkg, 'ocaml-mixed-executable', i[0])
|
|
|
|
if bin_info.readelf_error:
|
|
pass
|
|
elif not bin_info.needed and \
|
|
not (bin_info.soname and \
|
|
ldso_soname_regex.search(bin_info.soname)):
|
|
if shared_object_regex.search(i[1]):
|
|
printError(pkg, 'shared-lib-without-dependency-information', i[0])
|
|
else:
|
|
printError(pkg, 'statically-linked-binary', i[0])
|
|
else:
|
|
# linked against libc ?
|
|
if not libc_regex.search(i[0]) and \
|
|
( not bin_info.soname or \
|
|
( not libc_regex.search(bin_info.soname) and \
|
|
not ldso_soname_regex.search(bin_info.soname))):
|
|
found_libc = 0
|
|
for lib in bin_info.needed:
|
|
if libc_regex.search(lib):
|
|
found_libc = 1
|
|
break
|
|
if not found_libc:
|
|
if shared_object_regex.search(i[1]):
|
|
printError(pkg, 'library-not-linked-against-libc', i[0])
|
|
else:
|
|
printError(pkg, 'program-not-linked-against-libc', i[0])
|
|
|
|
# It could be useful to check these for others than
|
|
# shared libs only, but that has potential to
|
|
# generate lots of false positives and noise.
|
|
if is_shlib:
|
|
for s in bin_info.undef:
|
|
printWarning(pkg, 'undefined-non-weak-symbol', i[0], s)
|
|
for s in bin_info.unused:
|
|
printWarning(pkg, 'unused-direct-shlib-dependency', i[0], s)
|
|
|
|
if bin_info.stack:
|
|
if bin_info.exec_stack:
|
|
printWarning(pkg, 'executable-stack', i[0])
|
|
elif pkg.arch.endswith("86") or pkg.arch.startswith("pentium") or pkg.arch in ("athlon", "x86_64"):
|
|
printError(pkg, 'missing-PT_GNU_STACK-section', i[0])
|
|
|
|
else:
|
|
if reference_regex.search(i[0]):
|
|
lines = pkg.grep(invalid_dir_ref_regex, i[0])
|
|
if lines:
|
|
printError(pkg, 'invalid-directory-reference', i[0],
|
|
'(line %s)' % ", ".join(lines))
|
|
|
|
if has_lib != []:
|
|
if exec_files != []:
|
|
for f in exec_files:
|
|
printError(pkg, 'executable-in-library-package', f)
|
|
for f in files:
|
|
res = numeric_dir_regex.search(f)
|
|
fn = res and res.group(1) or f
|
|
if not f in exec_files and not so_regex.search(f) and not versioned_dir_regex.search(fn):
|
|
printError(pkg, 'non-versioned-file-in-library-package', f)
|
|
if version and version != -1 and pkg.name.find(version) == -1:
|
|
printError(pkg, 'incoherent-version-in-name', version)
|
|
|
|
if pkg.arch != 'noarch' and not multi_pkg:
|
|
if binary == 0:
|
|
printError(pkg, 'no-binary')
|
|
|
|
if has_usr_lib_file and not binary_in_usr_lib:
|
|
printError(pkg, 'only-non-binary-in-usr-lib')
|
|
|
|
# Create an object to enable the auto registration of the test
|
|
check = BinariesCheck()
|
|
|
|
# Add information about checks
|
|
if Config.info:
|
|
addDetails(
|
|
'arch-independent-package-contains-binary-or-object',
|
|
'''The package contains a binary or object file but is tagged
|
|
noarch.''',
|
|
|
|
'arch-dependent-file-in-usr-share',
|
|
'''This package installs an ELF binary in the /usr/share
|
|
hierarchy, which is reserved for architecture-independent files.''',
|
|
|
|
'binary-in-etc',
|
|
'''This package installs an ELF binary in /etc. Both the
|
|
FHS and the FSSTND forbid this.''',
|
|
|
|
# 'non-sparc32-binary',
|
|
# '',
|
|
|
|
'invalid-soname',
|
|
'''The soname of the library is neither of the form lib<libname>.so.<major> or
|
|
lib<libname>-<major>.so.''',
|
|
|
|
'invalid-ldconfig-symlink',
|
|
'''The symbolic link references the wrong file. It should reference
|
|
the shared library.''',
|
|
|
|
'no-ldconfig-symlink',
|
|
'''The package should not only include the shared library itself, but
|
|
also the symbolic link which ldconfig would produce. (This is
|
|
necessary, so that the link gets removed by rpm automatically when
|
|
the package gets removed, even if for some reason ldconfig would not be
|
|
run at package postinstall phase.)''',
|
|
|
|
'shlib-with-non-pic-code',
|
|
'''The listed shared libraries contain object code that was compiled
|
|
without -fPIC. All object code in shared libraries should be
|
|
recompiled separately from the static libraries with the -fPIC option.
|
|
|
|
Another common mistake that causes this problem is linking with
|
|
``gcc -Wl,-shared'' instead of ``gcc -shared''.''',
|
|
|
|
'binary-or-shlib-defines-rpath',
|
|
'''The binary or shared library defines `RPATH'. Usually this is a
|
|
bad thing because it hardcodes the path to search libraries and so
|
|
makes it difficult to move libraries around. Most likely you will find a
|
|
Makefile with a line like: gcc test.o -o test -Wl,--rpath. Also, sometimes
|
|
configure scripts provide a --disable-rpath flag to avoid this.''',
|
|
|
|
'statically-linked-binary',
|
|
'''The package installs a statically linked binary or object file.
|
|
|
|
Usually this is a packaging bug. If not, contact your rpmlint distributor
|
|
about this so that this error gets included in the exception file for rpmlint
|
|
and will not be flagged as a packaging bug in the future (or add it to your
|
|
local configuration if you installed rpmlint from the source tarball).''',
|
|
|
|
'executable-in-library-package',
|
|
'''The package mixes up libraries and executables. Mixing up these
|
|
both types of files makes upgrades quite impossible.''',
|
|
|
|
'non-versioned-file-in-library-package',
|
|
'''The package contains files in non versioned directories. This makes it
|
|
impossible to have multiple major versions of the libraries installed.
|
|
One solution can be to change the directories which contain the files
|
|
to subdirs of /usr/lib/<name>-<version> or /usr/share/<name>-<version>.
|
|
Another solution can be to include a version number in the file names
|
|
themselves.''',
|
|
|
|
'incoherent-version-in-name',
|
|
'''The package name should contain the major version of the library.''',
|
|
|
|
'invalid-directory-reference',
|
|
'This file contains a reference to /tmp or /home.',
|
|
|
|
'no-binary',
|
|
'''The package should be of the noarch architecture because it doesn't contain
|
|
any binaries.''',
|
|
|
|
# http://sources.redhat.com/ml/libc-alpha/2003-05/msg00034.html
|
|
'undefined-non-weak-symbol',
|
|
'''The binary contains undefined non-weak symbols. This may indicate improper
|
|
linkage; check that the binary has been linked as expected.''',
|
|
|
|
# http://www.redhat.com/archives/fedora-maintainers/2006-June/msg00176.html
|
|
'unused-direct-shlib-dependency',
|
|
'''The binary contains unused direct shared library dependencies. This may
|
|
indicate gratuitously bloated linkage; check that the binary has been linked
|
|
with the intended shared libraries only.''',
|
|
|
|
'only-non-binary-in-usr-lib',
|
|
'''There are only non binary files in /usr/lib so they should be in /usr/share.''',
|
|
|
|
'binaryinfo-readelf-failed',
|
|
'''Executing readelf on this file failed, all checks could not be run.''',
|
|
|
|
'binaryinfo-tail-failed',
|
|
'''Reading trailing bytes of this file failed, all checks could not be run.''',
|
|
|
|
'ldd-failed',
|
|
'''Executing ldd on this file failed, all checks could not be run.''',
|
|
|
|
'executable-stack',
|
|
'''The binary declares the stack as executable. Executable stack is usually an
|
|
error as it is only needed if the code contains GCC trampolines or similar
|
|
constructs which uses code on the stack. One common source for needlessly
|
|
executable stack cases are object files built from assembler files which
|
|
don\'t define a proper .note.GNU-stack section.''',
|
|
|
|
'missing-PT_GNU_STACK-section',
|
|
'''The binary lacks a PT_GNU_STACK section. This forces the dynamic linker to
|
|
make the stack executable. Usual suspects include use of a non-GNU linker or
|
|
an old GNU linker version.''',
|
|
|
|
'shared-lib-calls-exit',
|
|
'''This library package calls exit() or _exit(), probably in a non-fork()
|
|
context. Doing so from a library is strongly discouraged - when a library
|
|
function calls exit(), it prevents the calling program from handling the
|
|
error, reporting it to the user, closing files properly, and cleaning up any
|
|
state that the program has. It is preferred for the library to return an
|
|
actual error code and let the calling program decide how to handle the
|
|
situation.''',
|
|
|
|
'ocaml-mixed-executable',
|
|
'''Executables built with ocamlc -custom are deprecated. Packagers should ask
|
|
upstream maintainers to build these executables without the -custom option. If
|
|
this cannot be changed and the executable needs to be packaged in its current
|
|
form, make sure that rpmbuild does not strip it during the build, and on setups
|
|
that use prelink, make sure that prelink does not strip it either, usually by
|
|
placing a blacklist file in /etc/prelink.conf.d. For more information, see
|
|
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=256900#49''',
|
|
)
|
|
|
|
# BinariesCheck.py ends here
|
|
|
|
# Local variables:
|
|
# indent-tabs-mode: nil
|
|
# py-indent-offset: 4
|
|
# End:
|
|
# ex: ts=4 sw=4 et
|