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