Merge branch 'org.openembedded.dev' of git@git.openembedded.net:openembedded into...
[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         if "[mips32]" in line or "[mips64]" in line:
363             sane = True
364
365     if elf and not sane:
366         error_msg = "No GNU_HASH in the elf binary: '%s'" % path
367         return package_qa_handle_error(9, error_msg, name, path, d)
368
369     return True
370
371 def package_qa_check_staged(path,d):
372     """
373     Check staged la and pc files for sanity
374       -e.g. installed being false
375
376         As this is run after every stage we should be able
377         to find the one responsible for the errors easily even
378         if we look at every .pc and .la file
379     """
380     import os, bb
381
382     sane = True
383     tmpdir = bb.data.getVar('TMPDIR', d, True)
384     workdir = os.path.join(tmpdir, "work")
385
386     installed = "installed=yes"
387     iscrossnative = False
388     pkgconfigcheck = tmpdir
389     for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
390         if bb.data.inherits_class(s, d):
391             pkgconfigcheck = workdir
392             iscrossnative = True
393
394     # find all .la and .pc files
395     # read the content
396     # and check for stuff that looks wrong
397     for root, dirs, files in os.walk(path):
398         for file in files:
399             path = os.path.join(root,file)
400             if file[-2:] == "la":
401                 file_content = open(path).read()
402                 # Don't check installed status for native/cross packages
403                 if not iscrossnative:
404                     if installed in file_content:
405                         error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
406                         sane = package_qa_handle_error(5, error_msg, "staging", path, d)
407                 if workdir in file_content:
408                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
409                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
410             elif file[-2:] == "pc":
411                 file_content = open(path).read()
412                 if pkgconfigcheck in file_content:
413                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
414                     sane = package_qa_handle_error(6, error_msg, "staging", path, d)
415
416     return sane
417
418 # Walk over all files in a directory and call func
419 def package_qa_walk(path, funcs, package,d):
420     import bb, os
421     sane = True
422
423     #if this will throw an exception, then fix the dict above
424     target_os   = bb.data.getVar('TARGET_OS',   d, True)
425     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
426     (machine, osabi, abiversion, littleendian, bits32) \
427         = package_qa_get_machine_dict()[target_os][target_arch]
428
429     for root, dirs, files in os.walk(path):
430         for file in files:
431             path = os.path.join(root,file)
432             elf = package_qa_get_elf(path, bits32)
433             try:
434                 elf.open()
435             except:
436                 elf = None
437             for func in funcs:
438                 if not func(path, package,d, elf):
439                     sane = False
440
441     return sane
442
443 def package_qa_check_rdepends(pkg, workdir, d):
444     import bb
445     sane = True
446     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
447         # Copied from package_ipk.bbclass
448         # boiler plate to update the data
449         localdata = bb.data.createCopy(d)
450         root = "%s/install/%s" % (workdir, pkg)
451
452         bb.data.setVar('ROOT', '', localdata) 
453         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
454         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
455         if not pkgname:
456             pkgname = pkg
457         bb.data.setVar('PKG', pkgname, localdata)
458
459         overrides = bb.data.getVar('OVERRIDES', localdata)
460         if not overrides:
461             raise bb.build.FuncFailed('OVERRIDES not defined')
462         overrides = bb.data.expand(overrides, localdata)
463         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
464
465         bb.data.update_data(localdata)
466
467         # Now check the RDEPENDS
468         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
469
470
471         # Now do the sanity check!!!
472         for rdepend in rdepends:
473             if "-dbg" in rdepend:
474                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
475                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
476
477     return sane
478
479 # The PACKAGE FUNC to scan each package
480 python do_package_qa () {
481     import bb
482     bb.note("DO PACKAGE QA")
483     workdir = bb.data.getVar('WORKDIR', d, True)
484     packages = bb.data.getVar('PACKAGES',d, True)
485
486     # no packages should be scanned
487     if not packages:
488         return
489
490     checks = [package_qa_check_rpath, package_qa_check_devdbg,
491               package_qa_check_perm, package_qa_check_arch,
492               package_qa_check_desktop, package_qa_hash_style]
493     walk_sane = True
494     rdepends_sane = True
495     for package in packages.split():
496         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
497             bb.note("Package: %s (skipped)" % package)
498             continue
499
500         bb.note("Checking Package: %s" % package)
501         path = "%s/install/%s" % (workdir, package)
502         if not package_qa_walk(path, checks, package, d):
503             walk_sane  = False
504         if not package_qa_check_rdepends(package, workdir, d):
505             rdepends_sane = False
506
507     if not walk_sane or not rdepends_sane:
508         bb.fatal("QA run found fatal errors. Please consider fixing them.")
509     bb.note("DONE with PACKAGE QA")
510 }
511
512
513 # The Staging Func, to check all staging
514 addtask qa_staging after do_populate_staging before do_build
515 python do_qa_staging() {
516     bb.note("QA checking staging")
517
518     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
519         bb.fatal("QA staging was broken by the package built above")
520 }
521
522 # Check broken config.log files
523 addtask qa_configure after do_configure before do_compile
524 python do_qa_configure() {
525     bb.note("Checking sanity of the config.log file")
526     import os
527     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
528         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
529                     os.path.join(root,"config.log")
530         if "config.log" in files:
531             if os.system(statement) == 0:
532                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
533 Rerun configure task after fixing this. The path was '%s'""" % root)
534 }