MINI2440:[HACK] Assorted hacks to remove failure points
[openembedded/mini2440.git] / classes / insane.bbclass
blobc93a5c34fd2ca411354174ed4c4abf4724fe94b3
1 # BB Class inspired by ebuild.sh
3 # This class will test files after installation for certain
4 # security issues and other kind of issues.
6 # Checks we do:
7 #  -Check the ownership and permissions
8 #  -Check the RUNTIME path for the $TMPDIR
9 #  -Check if .la files wrongly point to workdir
10 #  -Check if .pc files wrongly point to workdir
11 #  -Check if packages contains .debug directories or .so files
12 #   where they should be in -dev or -dbg
13 #  -Check if config.log contains traces to broken autoconf tests
17 # We need to have the scanelf utility as soon as
18 # possible and this is contained within the pax-utils-native.
19 # The package.bbclass can help us here.
21 inherit package
22 PACKAGE_DEPENDS += "pax-utils-native desktop-file-utils-native"
23 PACKAGEFUNCS += " do_package_qa "
27 # dictionary for elf headers
29 # feel free to add and correct.
31 #           TARGET_OS  TARGET_ARCH   MACHINE, OSABI, ABIVERSION, Little Endian, 32bit?
32 def package_qa_get_machine_dict():
33     return {
34             "linux" : { 
35                         "arm" :       (40,    97,    0,          True,          True),
36                         "armeb":      (40,    97,    0,          False,         True),
37                         "powerpc":    (20,     0,    0,          False,         True),
38                         "i386":       ( 3,     0,    0,          True,          True),
39                         "i486":       ( 3,     0,    0,          True,          True),
40                         "i586":       ( 3,     0,    0,          True,          True),
41                         "i686":       ( 3,     0,    0,          True,          True),
42                         "x86_64":     (62,     0,    0,          True,          False),
43                         "ia64":       (50,     0,    0,          True,          False),
44                         "alpha":      (36902,  0,    0,          True,          False),
45                         "hppa":       (15,     3,    0,          False,         True),
46                         "m68k":       ( 4,     0,    0,          False,         True),
47                         "mips":       ( 8,     0,    0,          False,         True),
48                         "mipsel":     ( 8,     0,    0,          True,          True),
49                         "s390":       (22,     0,    0,          False,         True),
50                         "sh4":        (42,     0,    0,          True,          True),
51                         "sparc":      ( 2,     0,    0,          False,         True),
52                       },
53             "linux-uclibc" : { 
54                         "arm" :       (  40,    97,    0,          True,          True),
55                         "armeb":      (  40,    97,    0,          False,         True),
56                         "powerpc":    (  20,     0,    0,          False,         True),
57                         "i386":       (   3,     0,    0,          True,          True),
58                         "i486":       (   3,     0,    0,          True,          True),
59                         "i586":       (   3,     0,    0,          True,          True),
60                         "i686":       (   3,     0,    0,          True,          True),
61                         "mipsel":     (   8,     0,    0,          True,          True),
62                         "avr32":      (6317,     0,    0,          False,         True),
63                         "sh4":        (42,       0,    0,          True,          True),
65                       },
66             "uclinux-uclibc" : {
67                         "bfin":       ( 106,     0,    0,          True,         True),
68                       }, 
69             "linux-gnueabi" : {
70                         "arm" :       (40,     0,    0,          True,          True),
71                         "armeb" :     (40,     0,    0,          False,         True),
72                       },
73             "linux-uclibcgnueabi" : {
74                         "arm" :       (40,     0,    0,          True,          True),
75                         "armeb" :     (40,     0,    0,          False,         True),
76                       },
77             "linux-gnuspe" : {
78                         "powerpc":    (20,     0,    0,          False,         True),
79                       },
81        }
83 # factory for a class, embedded in a method
84 def package_qa_get_elf(path, bits32):
85     class ELFFile:
86         EI_NIDENT = 16
88         EI_CLASS      = 4
89         EI_DATA       = 5
90         EI_VERSION    = 6
91         EI_OSABI      = 7
92         EI_ABIVERSION = 8
94         # possible values for EI_CLASS
95         ELFCLASSNONE = 0
96         ELFCLASS32   = 1
97         ELFCLASS64   = 2
99         # possible value for EI_VERSION
100         EV_CURRENT   = 1
102         # possible values for EI_DATA
103         ELFDATANONE  = 0
104         ELFDATA2LSB  = 1
105         ELFDATA2MSB  = 2
107         def my_assert(self, expectation, result):
108             if not expectation == result:
109                 #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
110                 raise Exception("This does not work as expected")
112         def __init__(self, name):
113             self.name = name
115         def open(self):
116             self.file = file(self.name, "r")
117             self.data = self.file.read(ELFFile.EI_NIDENT+4)
119             self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
120             self.my_assert(self.data[0], chr(0x7f) )
121             self.my_assert(self.data[1], 'E')
122             self.my_assert(self.data[2], 'L')
123             self.my_assert(self.data[3], 'F')
124             if bits32 :
125                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
126             else:
127                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
128             self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
130             self.sex = self.data[ELFFile.EI_DATA]
131             if self.sex == chr(ELFFile.ELFDATANONE):
132                 raise Exception("self.sex == ELFDATANONE")
133             elif self.sex == chr(ELFFile.ELFDATA2LSB):
134                 self.sex = "<"
135             elif self.sex == chr(ELFFile.ELFDATA2MSB):
136                 self.sex = ">"
137             else:
138                 raise Exception("Unknown self.sex")
140         def osAbi(self):
141             return ord(self.data[ELFFile.EI_OSABI])
143         def abiVersion(self):
144             return ord(self.data[ELFFile.EI_ABIVERSION])
146         def isLittleEndian(self):
147             return self.sex == "<"
149         def isBigEngian(self):
150             return self.sex == ">"
152         def machine(self):
153             """
154             We know the sex stored in self.sex and we
155             know the position
156             """
157             import struct
158             (a,) = struct.unpack(self.sex+"H", self.data[18:20])
159             return a
161     return ELFFile(path)
164 # Known Error classes
165 # 0 - non dev contains .so
166 # 1 - package contains a dangerous RPATH
167 # 2 - package depends on debug package
168 # 3 - non dbg contains .so
169 # 4 - wrong architecture
170 # 5 - .la contains installed=yes or reference to the workdir
171 # 6 - .pc contains reference to /usr/include or workdir
172 # 7 - the desktop file is not valid
173 # 8 - .la contains reference to the workdir
174 # 9 - LDFLAGS ignored
176 def package_qa_clean_path(path,d):
177     """ Remove the common prefix from the path. In this case it is the TMPDIR"""
178     import bb
179     return path.replace(bb.data.getVar('TMPDIR',d,True),"")
181 def package_qa_make_fatal_error(error_class, name, path,d):
182     """
183     decide if an error is fatal
185     TODO: Load a whitelist of known errors
186     """
187     return not error_class in [0, 5, 7, 9]
189 def package_qa_write_error(error_class, name, path, d):
190     """
191     Log the error
192     """
193     import bb, os
194     if not bb.data.getVar('QA_LOG', d):
195         bb.note("a QA error occured but will not be logged because QA_LOG is not set")
196         return
198     ERROR_NAMES =[
199         "non dev contains .so",
200         "package contains RPATH",
201         "package depends on debug package",
202         "non dbg contains .debug",
203         "wrong architecture",
204         "evil hides inside the .la",
205         "evil hides inside the .pc",
206         "the desktop file is not valid",
207         ".la contains reference to the workdir",
208         "LDFLAGS ignored",
209     ]
211     log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
212     f = file( log_path, "a+")
213     print >> f, "%s, %s, %s" % \
214              (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
215     f.close()
217 def package_qa_handle_error(error_class, error_msg, name, path, d):
218     import bb
219     bb.error("QA Issue with %s: %s" % (name, error_msg))
220     package_qa_write_error(error_class, name, path, d)
221     return not package_qa_make_fatal_error(error_class, name, path, d)
223 def package_qa_check_rpath(file,name,d, elf):
224     """
225     Check for dangerous RPATHs
226     """
227     if not elf:
228         return True
230     import bb, os
231     sane = True
232     scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
233     bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
234     bad_dir_test = bb.data.getVar('TMPDIR', d, True)
235     if not os.path.exists(scanelf):
236         bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
238     if not bad_dir in bb.data.getVar('WORKDIR', d, True):
239         bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
241     output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
242     txt    = output.readline().split()
243     for line in txt:
244         if bad_dir in line:
245             error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
246             sane = package_qa_handle_error(1, error_msg, name, file, d)
248     return sane
250 def package_qa_check_devdbg(path, name,d, elf):
251     """
252     Check for debug remains inside the binary or
253     non dev packages containing
254     """
256     import bb, os
257     sane = True
259     if not "-dev" in name:
260         if path[-3:] == ".so" and os.path.islink(path):
261             error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
262                      (name, package_qa_clean_path(path,d))
263             sane = package_qa_handle_error(0, error_msg, name, path, d)
265     if not "-dbg" in name:
266         if '.debug' in path:
267             error_msg = "non debug package contains .debug directory: %s path %s" % \
268                      (name, package_qa_clean_path(path,d))
269             sane = package_qa_handle_error(3, error_msg, name, path, d)
271     return sane
273 def package_qa_check_perm(path,name,d, elf):
274     """
275     Check the permission of files
276     """
277     sane = True
278     return sane
280 def package_qa_check_arch(path,name,d, elf):
281     """
282     Check if archs are compatible
283     """
284     if not elf:
285         return True
287     import bb, os
288     sane = True
289     target_os   = bb.data.getVar('TARGET_OS',   d, True)
290     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
292     # FIXME: Cross package confuse this check, so just skip them
293     for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
294         if bb.data.inherits_class(s, d):
295             return True
297     # avoid following links to /usr/bin (e.g. on udev builds)
298     # we will check the files pointed to anyway...
299     if os.path.islink(path):
300         return True
302     #if this will throw an exception, then fix the dict above
303     (machine, osabi, abiversion, littleendian, bits32) \
304         = package_qa_get_machine_dict()[target_os][target_arch]
306     # Check the architecture and endiannes of the binary
307     if not machine == elf.machine():
308         error_msg = "Architecture did not match (%d to %d) on %s" % \
309                  (machine, elf.machine(), package_qa_clean_path(path,d))
310         sane = package_qa_handle_error(4, error_msg, name, path, d)
311     elif not littleendian == elf.isLittleEndian():
312         error_msg = "Endiannes did not match (%d to %d) on %s" % \
313                  (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
314         sane = package_qa_handle_error(4, error_msg, name, path, d)
316     return sane
318 def package_qa_check_desktop(path, name, d, elf):
319     """
320     Run all desktop files through desktop-file-validate.
321     """
322     import bb, os
323     sane = True
324     if path.endswith(".desktop"):
325         output = os.popen("desktop-file-validate %s" % path)
326         # This only produces output on errors
327         for l in output:
328             sane = package_qa_handle_error(7, l.strip(), name, path, d)
330     return sane
332 def package_qa_hash_style(path, name, d, elf):
333     """
334     Check if the binary has the right hash style...
335     """
336     import bb, os
338     if not elf:
339         return True
341     if os.path.islink(path):
342         return True
344     gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
345     if not gnu_hash:
346         gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
347     if not gnu_hash:
348         return True
350     objdump = bb.data.getVar('OBJDUMP', d, True)
351     env_path = bb.data.getVar('PATH', d, True)
353     sane = True
354     elf = False
355     # A bit hacky. We do not know if path is an elf binary or not
356     # we will search for 'NEEDED' or 'INIT' as this should be printed...
357     # and come before the HASH section (guess!!!) and works on split out
358     # debug symbols too
359     for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
360         if "NEEDED" in line or "INIT" in line:
361             sane = False
362             elf = True
363         if "GNU_HASH" in line:
364             sane = True
365         if "[mips32]" in line or "[mips64]" in line:
366             sane = True
368     if elf and not sane:
369         error_msg = "No GNU_HASH in the elf binary: '%s'" % path
370         return package_qa_handle_error(9, error_msg, name, path, d)
372     return True
374 def package_qa_check_staged(path,d):
375     """
376     Check staged la and pc files for sanity
377       -e.g. installed being false
379         As this is run after every stage we should be able
380         to find the one responsible for the errors easily even
381         if we look at every .pc and .la file
382     """
383     import os, bb
385     sane = True
386     tmpdir = bb.data.getVar('TMPDIR', d, True)
387     workdir = os.path.join(tmpdir, "work")
389     installed = "installed=yes"
390     iscrossnative = False
391     pkgconfigcheck = tmpdir
392     for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
393         if bb.data.inherits_class(s, d):
394             pkgconfigcheck = workdir
395             iscrossnative = True
397     # find all .la and .pc files
398     # read the content
399     # and check for stuff that looks wrong
400     for root, dirs, files in os.walk(path):
401         for file in files:
402             path = os.path.join(root,file)
403             if file[-2:] == "la":
404                 file_content = open(path).read()
405                 # Don't check installed status for native/cross packages
406                 if not iscrossnative:
407                     if installed in file_content:
408                         error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
409                         sane = package_qa_handle_error(5, error_msg, "staging", path, d)
410                 if workdir in file_content:
411                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
412                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
413             elif file[-2:] == "pc":
414                 file_content = open(path).read()
415                 if pkgconfigcheck in file_content:
416                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
417                     #sane = package_qa_handle_error(6, error_msg, "staging", path, d)
419     return sane
421 # Walk over all files in a directory and call func
422 def package_qa_walk(path, funcs, package,d):
423     import bb, os
424     sane = True
426     #if this will throw an exception, then fix the dict above
427     target_os   = bb.data.getVar('TARGET_OS',   d, True)
428     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
429     (machine, osabi, abiversion, littleendian, bits32) \
430         = package_qa_get_machine_dict()[target_os][target_arch]
432     for root, dirs, files in os.walk(path):
433         for file in files:
434             path = os.path.join(root,file)
435             elf = package_qa_get_elf(path, bits32)
436             try:
437                 elf.open()
438             except:
439                 elf = None
440             for func in funcs:
441                 if not func(path, package,d, elf):
442                     sane = False
444     return sane
446 def package_qa_check_rdepends(pkg, workdir, d):
447     import bb
448     sane = True
449     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
450         # Copied from package_ipk.bbclass
451         # boiler plate to update the data
452         localdata = bb.data.createCopy(d)
453         root = "%s/install/%s" % (workdir, pkg)
455         bb.data.setVar('ROOT', '', localdata) 
456         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
457         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
458         if not pkgname:
459             pkgname = pkg
460         bb.data.setVar('PKG', pkgname, localdata)
462         overrides = bb.data.getVar('OVERRIDES', localdata)
463         if not overrides:
464             raise bb.build.FuncFailed('OVERRIDES not defined')
465         overrides = bb.data.expand(overrides, localdata)
466         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
468         bb.data.update_data(localdata)
470         # Now check the RDEPENDS
471         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
474         # Now do the sanity check!!!
475         for rdepend in rdepends:
476             if "-dbg" in rdepend:
477                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
478                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
480     return sane
482 # The PACKAGE FUNC to scan each package
483 python do_package_qa () {
484     import bb
485     bb.debug(2, "DO PACKAGE QA")
486     workdir = bb.data.getVar('WORKDIR', d, True)
487     packages = bb.data.getVar('PACKAGES',d, True)
489     # no packages should be scanned
490     if not packages:
491         return
493     checks = [package_qa_check_rpath, package_qa_check_devdbg,
494               package_qa_check_perm, package_qa_check_arch,
495               package_qa_check_desktop, package_qa_hash_style]
496     walk_sane = True
497     rdepends_sane = True
498     for package in packages.split():
499         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
500             bb.note("package %s skipped" % package)
501             continue
503         bb.debug(1, "Checking Package: %s" % package)
504         path = "%s/install/%s" % (workdir, package)
505         if not package_qa_walk(path, checks, package, d):
506             walk_sane  = False
507         if not package_qa_check_rdepends(package, workdir, d):
508             rdepends_sane = False
510     if not walk_sane or not rdepends_sane:
511         bb.fatal("QA run found fatal errors. Please consider fixing them.")
512     bb.debug(2, "DONE with PACKAGE QA")
516 # The Staging Func, to check all staging
517 addtask qa_staging after do_populate_staging before do_build
518 python do_qa_staging() {
519     bb.debug(2, "QA checking staging")
521     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
522         bb.fatal("QA staging was broken by the package built above")
525 # Check broken config.log files
526 addtask qa_configure after do_configure before do_compile
527 python do_qa_configure() {
528     bb.debug(1, "Checking sanity of the config.log file")
529     import os
530     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
531         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
532                     os.path.join(root,"config.log")
533         if "config.log" in files:
534             if os.system(statement) == 0:
535                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
536 Rerun configure task after fixing this. The path was '%s'""" % root)