merge of '0c8de9825ca55a0e846e8c00dd29764c73349c6f'
[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                       },
64             "uclinux-uclibc" : {
65                         "bfin":       ( 106,     0,    0,          True,         True),
66                       }, 
67             "linux-gnueabi" : {
68                         "arm" :       (40,     0,    0,          True,          True),
69                         "armeb" :     (40,     0,    0,          False,         True),
70                       },
71             "linux-uclibcgnueabi" : {
72                         "arm" :       (40,     0,    0,          True,          True),
73                         "armeb" :     (40,     0,    0,          False,         True),
74                       },
75
76        }
77
78 # factory for a class, embedded in a method
79 def package_qa_get_elf(path, bits32):
80     class ELFFile:
81         EI_NIDENT = 16
82
83         EI_CLASS      = 4
84         EI_DATA       = 5
85         EI_VERSION    = 6
86         EI_OSABI      = 7
87         EI_ABIVERSION = 8
88
89         # possible values for EI_CLASS
90         ELFCLASSNONE = 0
91         ELFCLASS32   = 1
92         ELFCLASS64   = 2
93
94         # possible value for EI_VERSION
95         EV_CURRENT   = 1
96
97         # possible values for EI_DATA
98         ELFDATANONE  = 0
99         ELFDATA2LSB  = 1
100         ELFDATA2MSB  = 2
101
102         def my_assert(self, expectation, result):
103             if not expectation == result:
104                 #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
105                 raise Exception("This does not work as expected")
106
107         def __init__(self, name):
108             self.name = name
109
110         def open(self):
111             self.file = file(self.name, "r")
112             self.data = self.file.read(ELFFile.EI_NIDENT+4)
113
114             self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
115             self.my_assert(self.data[0], chr(0x7f) )
116             self.my_assert(self.data[1], 'E')
117             self.my_assert(self.data[2], 'L')
118             self.my_assert(self.data[3], 'F')
119             if bits32 :
120                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
121             else:
122                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
123             self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
124
125             self.sex = self.data[ELFFile.EI_DATA]
126             if self.sex == chr(ELFFile.ELFDATANONE):
127                 raise Exception("self.sex == ELFDATANONE")
128             elif self.sex == chr(ELFFile.ELFDATA2LSB):
129                 self.sex = "<"
130             elif self.sex == chr(ELFFile.ELFDATA2MSB):
131                 self.sex = ">"
132             else:
133                 raise Exception("Unknown self.sex")
134
135         def osAbi(self):
136             return ord(self.data[ELFFile.EI_OSABI])
137
138         def abiVersion(self):
139             return ord(self.data[ELFFile.EI_ABIVERSION])
140
141         def isLittleEndian(self):
142             return self.sex == "<"
143
144         def isBigEngian(self):
145             return self.sex == ">"
146
147         def machine(self):
148             """
149             We know the sex stored in self.sex and we
150             know the position
151             """
152             import struct
153             (a,) = struct.unpack(self.sex+"H", self.data[18:20])
154             return a
155
156     return ELFFile(path)
157
158
159 # Known Error classes
160 # 0 - non dev contains .so
161 # 1 - package contains a dangerous RPATH
162 # 2 - package depends on debug package
163 # 3 - non dbg contains .so
164 # 4 - wrong architecture
165 # 5 - .la contains installed=yes
166 # 6 - .pc contains reference to /usr/include or workdir
167 # 7 - the desktop file is not valid
168 # 8 - .la contains reference to the workdir
169
170 def package_qa_clean_path(path,d):
171     """ Remove the common prefix from the path. In this case it is the TMPDIR"""
172     import bb
173     return path.replace(bb.data.getVar('TMPDIR',d,True),"")
174
175 def package_qa_make_fatal_error(error_class, name, path,d):
176     """
177     decide if an error is fatal
178
179     TODO: Load a whitelist of known errors
180     """
181     return not error_class in [0, 5, 7, 8]
182
183 def package_qa_write_error(error_class, name, path, d):
184     """
185     Log the error
186     """
187     import bb, os
188     if not bb.data.getVar('QA_LOG', d):
189         bb.note("a QA error occured but will not be logged because QA_LOG is not set")
190         return
191
192     ERROR_NAMES =[
193         "non dev contains .so",
194         "package contains RPATH",
195         "package depends on debug package",
196         "non dbg contains .debug",
197         "wrong architecture",
198         "evil hides inside the .la",
199         "evil hides inside the .pc",
200         "the desktop file is not valid",
201         ".la contains reference to the workdir",
202     ]
203
204     log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
205     f = file( log_path, "a+")
206     print >> f, "%s, %s, %s" % \
207              (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
208     f.close()
209
210 def package_qa_handle_error(error_class, error_msg, name, path, d):
211     import bb
212     bb.error("QA Issue: %s" % error_msg)
213     package_qa_write_error(error_class, name, path, d)
214     return not package_qa_make_fatal_error(error_class, name, path, d)
215
216 def package_qa_check_rpath(file,name,d):
217     """
218     Check for dangerous RPATHs
219     """
220     import bb, os
221     sane = True
222     scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
223     bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
224     bad_dir_test = bb.data.getVar('TMPDIR', d, True)
225     if not os.path.exists(scanelf):
226         bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
227
228     if not bad_dir in bb.data.getVar('WORKDIR', d, True):
229         bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
230
231     output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
232     txt    = output.readline().split()
233     for line in txt:
234         if bad_dir in line:
235             error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
236             sane = package_qa_handle_error(1, error_msg, name, file, d)
237
238     return sane
239
240 def package_qa_check_devdbg(path, name,d):
241     """
242     Check for debug remains inside the binary or
243     non dev packages containing
244     """
245
246     import bb, os
247     sane = True
248
249     if not "-dev" in name:
250         if path[-3:] == ".so" and os.path.islink(path):
251             error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
252                      (name, package_qa_clean_path(path,d))
253             sane = package_qa_handle_error(0, error_msg, name, path, d)
254
255     if not "-dbg" in name:
256         if '.debug' in path:
257             error_msg = "non debug package contains .debug directory: %s path %s" % \
258                      (name, package_qa_clean_path(path,d))
259             sane = package_qa_handle_error(3, error_msg, name, path, d)
260
261     return sane
262
263 def package_qa_check_perm(path,name,d):
264     """
265     Check the permission of files
266     """
267     sane = True
268     return sane
269
270 def package_qa_check_arch(path,name,d):
271     """
272     Check if archs are compatible
273     """
274     import bb, os
275     sane = True
276     target_os   = bb.data.getVar('TARGET_OS',   d, True)
277     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
278
279     # FIXME: Cross package confuse this check, so just skip them
280     if bb.data.inherits_class('cross', d) or bb.data.inherits_class('sdk', d):
281         return True
282
283     # avoid following links to /usr/bin (e.g. on udev builds)
284     # we will check the files pointed to anyway...
285     if os.path.islink(path):
286         return True
287
288     #if this will throw an exception, then fix the dict above
289     (machine, osabi, abiversion, littleendian, bits32) \
290         = package_qa_get_machine_dict()[target_os][target_arch]
291     elf = package_qa_get_elf(path, bits32)
292     try:
293         elf.open()
294     except:
295         return True
296
297     # Check the architecture and endiannes of the binary
298     if not machine == elf.machine():
299         error_msg = "Architecture did not match (%d to %d) on %s" % \
300                  (machine, elf.machine(), package_qa_clean_path(path,d))
301         sane = package_qa_handle_error(4, error_msg, name, path, d)
302     elif not littleendian == elf.isLittleEndian():
303         error_msg = "Endiannes did not match (%d to %d) on %s" % \
304                  (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
305         sane = package_qa_handle_error(4, error_msg, name, path, d)
306
307     return sane
308
309 def package_qa_check_desktop(path, name, d):
310     """
311     Run all desktop files through desktop-file-validate.
312     """
313     import bb, os
314     sane = True
315     if path.endswith(".desktop"):
316         validate = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True), \
317                                 'desktop-file-validate')
318         output = os.popen("%s %s" % (validate, path))
319         for l in output:
320             sane = package_qa_handle_error(7, l.strip(), name, path, d)
321
322     return sane
323
324 def package_qa_check_staged(path,d):
325     """
326     Check staged la and pc files for sanity
327       -e.g. installed being false
328
329         As this is run after every stage we should be able
330         to find the one responsible for the errors easily even
331         if we look at every .pc and .la file
332     """
333     import os, bb
334
335     sane = True
336     tmpdir = bb.data.getVar('TMPDIR', d, True)
337     workdir = os.path.join(tmpdir, "work")
338
339     if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
340         installed = "installed=no"
341         pkgconfigcheck = workdir
342     else:
343         installed = "installed=yes"
344         pkgconfigcheck = tmpdir
345
346     # find all .la and .pc files
347     # read the content
348     # and check for stuff that looks wrong
349     for root, dirs, files in os.walk(path):
350         for file in files:
351             path = os.path.join(root,file)
352             if file[-2:] == "la":
353                 file_content = open(path).read()
354                 if installed in file_content:
355                     error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
356                     sane = package_qa_handle_error(5, error_msg, "staging", path, d)
357                 if workdir in file_content:
358                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
359                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
360             elif file[-2:] == "pc":
361                 file_content = open(path).read()
362                 if pkgconfigcheck in file_content:
363                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
364                     sane = package_qa_handle_error(6, error_msg, "staging", path, d)
365
366     return sane
367
368 # Walk over all files in a directory and call func
369 def package_qa_walk(path, funcs, package,d):
370     import os
371     sane = True
372
373     for root, dirs, files in os.walk(path):
374         for file in files:
375             path = os.path.join(root,file)
376             for func in funcs:
377                 if not func(path, package,d):
378                     sane = False
379
380     return sane
381
382 def package_qa_check_rdepends(pkg, workdir, d):
383     import bb
384     sane = True
385     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
386         # Copied from package_ipk.bbclass
387         # boiler plate to update the data
388         localdata = bb.data.createCopy(d)
389         root = "%s/install/%s" % (workdir, pkg)
390
391         bb.data.setVar('ROOT', '', localdata) 
392         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
393         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
394         if not pkgname:
395             pkgname = pkg
396         bb.data.setVar('PKG', pkgname, localdata)
397
398         overrides = bb.data.getVar('OVERRIDES', localdata)
399         if not overrides:
400             raise bb.build.FuncFailed('OVERRIDES not defined')
401         overrides = bb.data.expand(overrides, localdata)
402         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
403
404         bb.data.update_data(localdata)
405
406         # Now check the RDEPENDS
407         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
408
409
410         # Now do the sanity check!!!
411         for rdepend in rdepends:
412             if "-dbg" in rdepend:
413                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
414                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
415
416     return sane
417
418 # The PACKAGE FUNC to scan each package
419 python do_package_qa () {
420     bb.note("DO PACKAGE QA")
421     workdir = bb.data.getVar('WORKDIR', d, True)
422     packages = bb.data.getVar('PACKAGES',d, True)
423
424     # no packages should be scanned
425     if not packages:
426         return
427
428     checks = [package_qa_check_rpath, package_qa_check_devdbg,
429               package_qa_check_perm, package_qa_check_arch,
430               package_qa_check_desktop]
431     walk_sane = True
432     rdepends_sane = True
433     for package in packages.split():
434         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
435             bb.note("Package: %s (skipped)" % package)
436             continue
437
438         bb.note("Checking Package: %s" % package)
439         path = "%s/install/%s" % (workdir, package)
440         if not package_qa_walk(path, checks, package, d):
441             walk_sane  = False
442         if not package_qa_check_rdepends(package, workdir, d):
443             rdepends_sane = False
444
445     if not walk_sane or not rdepends_sane:
446         bb.fatal("QA run found fatal errors. Please consider fixing them.")
447     bb.note("DONE with PACKAGE QA")
448 }
449
450
451 # The Staging Func, to check all staging
452 addtask qa_staging after do_populate_staging before do_build
453 python do_qa_staging() {
454     bb.note("QA checking staging")
455
456     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
457         bb.fatal("QA staging was broken by the package built above")
458 }
459
460 # Check broken config.log files
461 addtask qa_configure after do_configure before do_compile
462 python do_qa_configure() {
463     bb.note("Checking sanity of the config.log file")
464     import os
465     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
466         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
467                     os.path.join(root,"config.log")
468         if "config.log" in files:
469             if os.system(statement) == 0:
470                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
471 Rerun configure task after fixing this. The path was '%s'""" % root)
472 }