insane.bbclass: Add handling for canadian classes
[vuplus_openembedded] / classes / insane.bbclass
1 # BB Class inspired by ebuild.sh
2 #
3 # This class will test files after installation for certain
4 # security issues and other kind of issues.
5 #
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
14
15
16 #
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.
20 #
21 inherit package
22 PACKAGE_DEPENDS += "pax-utils-native desktop-file-utils-native"
23 PACKAGEFUNCS += " do_package_qa "
24
25
26 #
27 # dictionary for elf headers
28 #
29 # feel free to add and correct.
30 #
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),
64
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                       },
80
81        }
82
83 # factory for a class, embedded in a method
84 def package_qa_get_elf(path, bits32):
85     class ELFFile:
86         EI_NIDENT = 16
87
88         EI_CLASS      = 4
89         EI_DATA       = 5
90         EI_VERSION    = 6
91         EI_OSABI      = 7
92         EI_ABIVERSION = 8
93
94         # possible values for EI_CLASS
95         ELFCLASSNONE = 0
96         ELFCLASS32   = 1
97         ELFCLASS64   = 2
98
99         # possible value for EI_VERSION
100         EV_CURRENT   = 1
101
102         # possible values for EI_DATA
103         ELFDATANONE  = 0
104         ELFDATA2LSB  = 1
105         ELFDATA2MSB  = 2
106
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")
111
112         def __init__(self, name):
113             self.name = name
114
115         def open(self):
116             self.file = file(self.name, "r")
117             self.data = self.file.read(ELFFile.EI_NIDENT+4)
118
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) )
129
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")
139
140         def osAbi(self):
141             return ord(self.data[ELFFile.EI_OSABI])
142
143         def abiVersion(self):
144             return ord(self.data[ELFFile.EI_ABIVERSION])
145
146         def isLittleEndian(self):
147             return self.sex == "<"
148
149         def isBigEngian(self):
150             return self.sex == ">"
151
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
160
161     return ELFFile(path)
162
163
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
175 def package_qa_clean_path(path,d):
176     """ Remove the common prefix from the path. In this case it is the TMPDIR"""
177     import bb
178     return path.replace(bb.data.getVar('TMPDIR',d,True),"")
179
180 def package_qa_make_fatal_error(error_class, name, path,d):
181     """
182     decide if an error is fatal
183
184     TODO: Load a whitelist of known errors
185     """
186     return not error_class in [0, 5, 7, 9]
187
188 def package_qa_write_error(error_class, name, path, d):
189     """
190     Log the error
191     """
192     import bb, os
193     if not bb.data.getVar('QA_LOG', d):
194         bb.note("a QA error occured but will not be logged because QA_LOG is not set")
195         return
196
197     ERROR_NAMES =[
198         "non dev contains .so",
199         "package contains RPATH",
200         "package depends on debug package",
201         "non dbg contains .debug",
202         "wrong architecture",
203         "evil hides inside the .la",
204         "evil hides inside the .pc",
205         "the desktop file is not valid",
206         ".la contains reference to the workdir",
207         "LDFLAGS ignored",
208     ]
209
210     log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
211     f = file( log_path, "a+")
212     print >> f, "%s, %s, %s" % \
213              (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
214     f.close()
215
216 def package_qa_handle_error(error_class, error_msg, name, path, d):
217     import bb
218     bb.error("QA Issue: %s" % error_msg)
219     package_qa_write_error(error_class, name, path, d)
220     return not package_qa_make_fatal_error(error_class, name, path, d)
221
222 def package_qa_check_rpath(file,name,d, elf):
223     """
224     Check for dangerous RPATHs
225     """
226     if not elf:
227         return True
228
229     import bb, os
230     sane = True
231     scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
232     bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
233     bad_dir_test = bb.data.getVar('TMPDIR', d, True)
234     if not os.path.exists(scanelf):
235         bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
236
237     if not bad_dir in bb.data.getVar('WORKDIR', d, True):
238         bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
239
240     output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
241     txt    = output.readline().split()
242     for line in txt:
243         if bad_dir in line:
244             error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
245             sane = package_qa_handle_error(1, error_msg, name, file, d)
246
247     return sane
248
249 def package_qa_check_devdbg(path, name,d, elf):
250     """
251     Check for debug remains inside the binary or
252     non dev packages containing
253     """
254
255     import bb, os
256     sane = True
257
258     if not "-dev" in name:
259         if path[-3:] == ".so" and os.path.islink(path):
260             error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
261                      (name, package_qa_clean_path(path,d))
262             sane = package_qa_handle_error(0, error_msg, name, path, d)
263
264     if not "-dbg" in name:
265         if '.debug' in path:
266             error_msg = "non debug package contains .debug directory: %s path %s" % \
267                      (name, package_qa_clean_path(path,d))
268             sane = package_qa_handle_error(3, error_msg, name, path, d)
269
270     return sane
271
272 def package_qa_check_perm(path,name,d, elf):
273     """
274     Check the permission of files
275     """
276     sane = True
277     return sane
278
279 def package_qa_check_arch(path,name,d, elf):
280     """
281     Check if archs are compatible
282     """
283     if not elf:
284         return True
285
286     import bb, os
287     sane = True
288     target_os   = bb.data.getVar('TARGET_OS',   d, True)
289     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
290
291     # FIXME: Cross package confuse this check, so just skip them
292     for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
293         if bb.data.inherits_class(s, d):
294             return True
295
296     # avoid following links to /usr/bin (e.g. on udev builds)
297     # we will check the files pointed to anyway...
298     if os.path.islink(path):
299         return True
300
301     #if this will throw an exception, then fix the dict above
302     (machine, osabi, abiversion, littleendian, bits32) \
303         = package_qa_get_machine_dict()[target_os][target_arch]
304
305     # Check the architecture and endiannes of the binary
306     if not machine == elf.machine():
307         error_msg = "Architecture did not match (%d to %d) on %s" % \
308                  (machine, elf.machine(), package_qa_clean_path(path,d))
309         sane = package_qa_handle_error(4, error_msg, name, path, d)
310     elif not littleendian == elf.isLittleEndian():
311         error_msg = "Endiannes did not match (%d to %d) on %s" % \
312                  (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
313         sane = package_qa_handle_error(4, error_msg, name, path, d)
314
315     return sane
316
317 def package_qa_check_desktop(path, name, d, elf):
318     """
319     Run all desktop files through desktop-file-validate.
320     """
321     import bb, os
322     sane = True
323     if path.endswith(".desktop"):
324         output = os.popen("desktop-file-validate %s" % path)
325         # This only produces output on errors
326         for l in output:
327             sane = package_qa_handle_error(7, l.strip(), name, path, d)
328
329     return sane
330
331 def package_qa_hash_style(path, name, d, elf):
332     """
333     Check if the binary has the right hash style...
334     """
335     import bb, os
336
337     if not elf:
338         return True
339
340     if os.path.islink(path):
341         return True
342
343     gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
344     if not gnu_hash:
345         gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
346
347     objdump = bb.data.getVar('OBJDUMP', d, True)
348     env_path = bb.data.getVar('PATH', d, True)
349
350     sane = True
351     elf = False
352     # A bit hacky. We do not know if path is an elf binary or not
353     # we will search for 'NEEDED' or 'INIT' as this should be printed...
354     # and come before the HASH section (guess!!!) and works on split out
355     # debug symbols too
356     for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
357         if "NEEDED" in line or "INIT" in line:
358             sane = False
359             elf = True
360         if "GNU_HASH" in line:
361             sane = True
362
363     if elf and not sane:
364         error_msg = "No GNU_HASH in the elf binary: '%s'" % path
365         return package_qa_handle_error(9, error_msg, name, path, d)
366
367     return True
368
369 def package_qa_check_staged(path,d):
370     """
371     Check staged la and pc files for sanity
372       -e.g. installed being false
373
374         As this is run after every stage we should be able
375         to find the one responsible for the errors easily even
376         if we look at every .pc and .la file
377     """
378     import os, bb
379
380     sane = True
381     tmpdir = bb.data.getVar('TMPDIR', d, True)
382     workdir = os.path.join(tmpdir, "work")
383
384     installed = "installed=yes"
385     iscrossnative = False
386     for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
387         if bb.data.inherits_class(s, d):
388             pkgconfigcheck = workdir
389             iscrossnative = True
390     else:
391         pkgconfigcheck = tmpdir
392
393     # find all .la and .pc files
394     # read the content
395     # and check for stuff that looks wrong
396     for root, dirs, files in os.walk(path):
397         for file in files:
398             path = os.path.join(root,file)
399             if file[-2:] == "la":
400                 file_content = open(path).read()
401                 # Don't check installed status for native/cross packages
402                 if not iscrossnative:
403                     if installed in file_content:
404                         error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
405                         sane = package_qa_handle_error(5, error_msg, "staging", path, d)
406                 if workdir in file_content:
407                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
408                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
409             elif file[-2:] == "pc":
410                 file_content = open(path).read()
411                 if pkgconfigcheck in file_content:
412                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
413                     sane = package_qa_handle_error(6, error_msg, "staging", path, d)
414
415     return sane
416
417 # Walk over all files in a directory and call func
418 def package_qa_walk(path, funcs, package,d):
419     import bb, os
420     sane = True
421
422     #if this will throw an exception, then fix the dict above
423     target_os   = bb.data.getVar('TARGET_OS',   d, True)
424     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
425     (machine, osabi, abiversion, littleendian, bits32) \
426         = package_qa_get_machine_dict()[target_os][target_arch]
427
428     for root, dirs, files in os.walk(path):
429         for file in files:
430             path = os.path.join(root,file)
431             elf = package_qa_get_elf(path, bits32)
432             try:
433                 elf.open()
434             except:
435                 elf = None
436             for func in funcs:
437                 if not func(path, package,d, elf):
438                     sane = False
439
440     return sane
441
442 def package_qa_check_rdepends(pkg, workdir, d):
443     import bb
444     sane = True
445     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
446         # Copied from package_ipk.bbclass
447         # boiler plate to update the data
448         localdata = bb.data.createCopy(d)
449         root = "%s/install/%s" % (workdir, pkg)
450
451         bb.data.setVar('ROOT', '', localdata) 
452         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
453         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
454         if not pkgname:
455             pkgname = pkg
456         bb.data.setVar('PKG', pkgname, localdata)
457
458         overrides = bb.data.getVar('OVERRIDES', localdata)
459         if not overrides:
460             raise bb.build.FuncFailed('OVERRIDES not defined')
461         overrides = bb.data.expand(overrides, localdata)
462         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
463
464         bb.data.update_data(localdata)
465
466         # Now check the RDEPENDS
467         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
468
469
470         # Now do the sanity check!!!
471         for rdepend in rdepends:
472             if "-dbg" in rdepend:
473                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
474                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
475
476     return sane
477
478 # The PACKAGE FUNC to scan each package
479 python do_package_qa () {
480     import bb
481     bb.note("DO PACKAGE QA")
482     workdir = bb.data.getVar('WORKDIR', d, True)
483     packages = bb.data.getVar('PACKAGES',d, True)
484
485     # no packages should be scanned
486     if not packages:
487         return
488
489     checks = [package_qa_check_rpath, package_qa_check_devdbg,
490               package_qa_check_perm, package_qa_check_arch,
491               package_qa_check_desktop, package_qa_hash_style]
492     walk_sane = True
493     rdepends_sane = True
494     for package in packages.split():
495         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
496             bb.note("Package: %s (skipped)" % package)
497             continue
498
499         bb.note("Checking Package: %s" % package)
500         path = "%s/install/%s" % (workdir, package)
501         if not package_qa_walk(path, checks, package, d):
502             walk_sane  = False
503         if not package_qa_check_rdepends(package, workdir, d):
504             rdepends_sane = False
505
506     if not walk_sane or not rdepends_sane:
507         bb.fatal("QA run found fatal errors. Please consider fixing them.")
508     bb.note("DONE with PACKAGE QA")
509 }
510
511
512 # The Staging Func, to check all staging
513 addtask qa_staging after do_populate_staging before do_build
514 python do_qa_staging() {
515     bb.note("QA checking staging")
516
517     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
518         bb.fatal("QA staging was broken by the package built above")
519 }
520
521 # Check broken config.log files
522 addtask qa_configure after do_configure before do_compile
523 python do_qa_configure() {
524     bb.note("Checking sanity of the config.log file")
525     import os
526     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
527         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
528                     os.path.join(root,"config.log")
529         if "config.log" in files:
530             if os.system(statement) == 0:
531                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
532 Rerun configure task after fixing this. The path was '%s'""" % root)
533 }