merge of '28a6e903d35d148bb1e7c2790ec279ef7fcd3c3f'
[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]
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     ]
208
209     log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
210     f = file( log_path, "a+")
211     print >> f, "%s, %s, %s" % \
212              (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
213     f.close()
214
215 def package_qa_handle_error(error_class, error_msg, name, path, d):
216     import bb
217     bb.error("QA Issue: %s" % error_msg)
218     package_qa_write_error(error_class, name, path, d)
219     return not package_qa_make_fatal_error(error_class, name, path, d)
220
221 def package_qa_check_rpath(file,name,d):
222     """
223     Check for dangerous RPATHs
224     """
225     import bb, os
226     sane = True
227     scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
228     bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
229     bad_dir_test = bb.data.getVar('TMPDIR', d, True)
230     if not os.path.exists(scanelf):
231         bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
232
233     if not bad_dir in bb.data.getVar('WORKDIR', d, True):
234         bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
235
236     output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
237     txt    = output.readline().split()
238     for line in txt:
239         if bad_dir in line:
240             error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
241             sane = package_qa_handle_error(1, error_msg, name, file, d)
242
243     return sane
244
245 def package_qa_check_devdbg(path, name,d):
246     """
247     Check for debug remains inside the binary or
248     non dev packages containing
249     """
250
251     import bb, os
252     sane = True
253
254     if not "-dev" in name:
255         if path[-3:] == ".so" and os.path.islink(path):
256             error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
257                      (name, package_qa_clean_path(path,d))
258             sane = package_qa_handle_error(0, error_msg, name, path, d)
259
260     if not "-dbg" in name:
261         if '.debug' in path:
262             error_msg = "non debug package contains .debug directory: %s path %s" % \
263                      (name, package_qa_clean_path(path,d))
264             sane = package_qa_handle_error(3, error_msg, name, path, d)
265
266     return sane
267
268 def package_qa_check_perm(path,name,d):
269     """
270     Check the permission of files
271     """
272     sane = True
273     return sane
274
275 def package_qa_check_arch(path,name,d):
276     """
277     Check if archs are compatible
278     """
279     import bb, os
280     sane = True
281     target_os   = bb.data.getVar('TARGET_OS',   d, True)
282     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
283
284     # FIXME: Cross package confuse this check, so just skip them
285     if bb.data.inherits_class('cross', d) or bb.data.inherits_class('sdk', d):
286         return True
287
288     # avoid following links to /usr/bin (e.g. on udev builds)
289     # we will check the files pointed to anyway...
290     if os.path.islink(path):
291         return True
292
293     #if this will throw an exception, then fix the dict above
294     (machine, osabi, abiversion, littleendian, bits32) \
295         = package_qa_get_machine_dict()[target_os][target_arch]
296     elf = package_qa_get_elf(path, bits32)
297     try:
298         elf.open()
299     except:
300         return True
301
302     # Check the architecture and endiannes of the binary
303     if not machine == elf.machine():
304         error_msg = "Architecture did not match (%d to %d) on %s" % \
305                  (machine, elf.machine(), package_qa_clean_path(path,d))
306         sane = package_qa_handle_error(4, error_msg, name, path, d)
307     elif not littleendian == elf.isLittleEndian():
308         error_msg = "Endiannes did not match (%d to %d) on %s" % \
309                  (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
310         sane = package_qa_handle_error(4, error_msg, name, path, d)
311
312     return sane
313
314 def package_qa_check_desktop(path, name, d):
315     """
316     Run all desktop files through desktop-file-validate.
317     """
318     import bb, os
319     sane = True
320     if path.endswith(".desktop"):
321         output = os.popen("desktop-file-validate %s" % path)
322         # This only produces output on errors
323         for l in output:
324             sane = package_qa_handle_error(7, l.strip(), name, path, d)
325
326     return sane
327
328 def package_qa_check_staged(path,d):
329     """
330     Check staged la and pc files for sanity
331       -e.g. installed being false
332
333         As this is run after every stage we should be able
334         to find the one responsible for the errors easily even
335         if we look at every .pc and .la file
336     """
337     import os, bb
338
339     sane = True
340     tmpdir = bb.data.getVar('TMPDIR', d, True)
341     workdir = os.path.join(tmpdir, "work")
342
343     installed = "installed=yes"
344     if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
345         pkgconfigcheck = workdir
346     else:
347         pkgconfigcheck = tmpdir
348
349     # find all .la and .pc files
350     # read the content
351     # and check for stuff that looks wrong
352     for root, dirs, files in os.walk(path):
353         for file in files:
354             path = os.path.join(root,file)
355             if file[-2:] == "la":
356                 file_content = open(path).read()
357                 # Don't check installed status for native/cross packages
358                 if not bb.data.inherits_class("native", d) and not bb.data.inherits_class("cross", d):
359                     if installed in file_content:
360                         error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
361                         sane = package_qa_handle_error(5, error_msg, "staging", path, d)
362                 if workdir in file_content:
363                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
364                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
365             elif file[-2:] == "pc":
366                 file_content = open(path).read()
367                 if pkgconfigcheck in file_content:
368                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
369                     sane = package_qa_handle_error(6, error_msg, "staging", path, d)
370
371     return sane
372
373 # Walk over all files in a directory and call func
374 def package_qa_walk(path, funcs, package,d):
375     import os
376     sane = True
377
378     for root, dirs, files in os.walk(path):
379         for file in files:
380             path = os.path.join(root,file)
381             for func in funcs:
382                 if not func(path, package,d):
383                     sane = False
384
385     return sane
386
387 def package_qa_check_rdepends(pkg, workdir, d):
388     import bb
389     sane = True
390     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
391         # Copied from package_ipk.bbclass
392         # boiler plate to update the data
393         localdata = bb.data.createCopy(d)
394         root = "%s/install/%s" % (workdir, pkg)
395
396         bb.data.setVar('ROOT', '', localdata) 
397         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
398         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
399         if not pkgname:
400             pkgname = pkg
401         bb.data.setVar('PKG', pkgname, localdata)
402
403         overrides = bb.data.getVar('OVERRIDES', localdata)
404         if not overrides:
405             raise bb.build.FuncFailed('OVERRIDES not defined')
406         overrides = bb.data.expand(overrides, localdata)
407         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
408
409         bb.data.update_data(localdata)
410
411         # Now check the RDEPENDS
412         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
413
414
415         # Now do the sanity check!!!
416         for rdepend in rdepends:
417             if "-dbg" in rdepend:
418                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
419                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
420
421     return sane
422
423 # The PACKAGE FUNC to scan each package
424 python do_package_qa () {
425     bb.note("DO PACKAGE QA")
426     workdir = bb.data.getVar('WORKDIR', d, True)
427     packages = bb.data.getVar('PACKAGES',d, True)
428
429     # no packages should be scanned
430     if not packages:
431         return
432
433     checks = [package_qa_check_rpath, package_qa_check_devdbg,
434               package_qa_check_perm, package_qa_check_arch,
435               package_qa_check_desktop]
436     walk_sane = True
437     rdepends_sane = True
438     for package in packages.split():
439         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
440             bb.note("Package: %s (skipped)" % package)
441             continue
442
443         bb.note("Checking Package: %s" % package)
444         path = "%s/install/%s" % (workdir, package)
445         if not package_qa_walk(path, checks, package, d):
446             walk_sane  = False
447         if not package_qa_check_rdepends(package, workdir, d):
448             rdepends_sane = False
449
450     if not walk_sane or not rdepends_sane:
451         bb.fatal("QA run found fatal errors. Please consider fixing them.")
452     bb.note("DONE with PACKAGE QA")
453 }
454
455
456 # The Staging Func, to check all staging
457 addtask qa_staging after do_populate_staging before do_build
458 python do_qa_staging() {
459     bb.note("QA checking staging")
460
461     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
462         bb.fatal("QA staging was broken by the package built above")
463 }
464
465 # Check broken config.log files
466 addtask qa_configure after do_configure before do_compile
467 python do_qa_configure() {
468     bb.note("Checking sanity of the config.log file")
469     import os
470     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
471         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
472                     os.path.join(root,"config.log")
473         if "config.log" in files:
474             if os.system(statement) == 0:
475                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
476 Rerun configure task after fixing this. The path was '%s'""" % root)
477 }