1 # BB Class inspired by ebuild.sh
3 # This class will test files after installation for certain
4 # security issues and other kind of issues.
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
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.
22 PACKAGE_DEPENDS += "pax-utils-native desktop-file-utils-native"
23 PACKAGEFUNCS += " do_package_qa "
27 # dictionary for elf headers
29 # feel free to add and correct.
31 # TARGET_OS TARGET_ARCH MACHINE, OSABI, ABIVERSION, Little Endian, 32bit?
32 def package_qa_get_machine_dict():
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),
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),
67 "bfin": ( 106, 0, 0, True, True),
70 "arm" : (40, 0, 0, True, True),
71 "armeb" : (40, 0, 0, False, True),
73 "linux-uclibcgnueabi" : {
74 "arm" : (40, 0, 0, True, True),
75 "armeb" : (40, 0, 0, False, True),
78 "powerpc": (20, 0, 0, False, True),
83 # factory for a class, embedded in a method
84 def package_qa_get_elf(path, bits32):
94 # possible values for EI_CLASS
99 # possible value for EI_VERSION
102 # possible values for EI_DATA
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")
112 def __init__(self, name):
116 self.file = file(self.name, "r")
117 self.data = self.file.read(ELFFile.EI_NIDENT+4)
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')
125 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
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) )
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):
135 elif self.sex == chr(ELFFile.ELFDATA2MSB):
138 raise Exception("Unknown self.sex")
141 return ord(self.data[ELFFile.EI_OSABI])
143 def abiVersion(self):
144 return ord(self.data[ELFFile.EI_ABIVERSION])
146 def isLittleEndian(self):
147 return self.sex == "<"
149 def isBigEngian(self):
150 return self.sex == ">"
154 We know the sex stored in self.sex and we
158 (a,) = struct.unpack(self.sex+"H", self.data[18:20])
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 # 9 - LDFLAGS ignored
176 def package_qa_clean_path(path,d):
177 """ Remove the common prefix from the path. In this case it is the TMPDIR"""
179 return path.replace(bb.data.getVar('TMPDIR',d,True),"")
181 def package_qa_make_fatal_error(error_class, name, path,d):
183 decide if an error is fatal
185 TODO: Load a whitelist of known errors
187 return not error_class in [0, 5, 7, 9]
189 def package_qa_write_error(error_class, name, path, d):
194 if not bb.data.getVar('QA_LOG', d):
195 bb.note("a QA error occured but will not be logged because QA_LOG is not set")
199 "non dev contains .so",
200 "package contains RPATH",
201 "package depends on debug package",
202 "non dbg contains .debug",
203 "wrong architecture",
204 "evil hides inside the .la",
205 "evil hides inside the .pc",
206 "the desktop file is not valid",
207 ".la contains reference to the workdir",
211 log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
212 f = file( log_path, "a+")
213 print >> f, "%s, %s, %s" % \
214 (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
217 def package_qa_handle_error(error_class, error_msg, name, path, d):
219 bb.error("QA Issue with %s: %s" % (name, error_msg))
220 package_qa_write_error(error_class, name, path, d)
221 return not package_qa_make_fatal_error(error_class, name, path, d)
223 def package_qa_check_rpath(file,name,d, elf):
225 Check for dangerous RPATHs
232 scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
233 bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
234 bad_dir_test = bb.data.getVar('TMPDIR', d, True)
235 if not os.path.exists(scanelf):
236 bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
238 if not bad_dir in bb.data.getVar('WORKDIR', d, True):
239 bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
241 output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
242 txt = output.readline().split()
245 error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
246 sane = package_qa_handle_error(1, error_msg, name, file, d)
250 def package_qa_check_devdbg(path, name,d, elf):
252 Check for debug remains inside the binary or
253 non dev packages containing
259 if not "-dev" in name:
260 if path[-3:] == ".so" and os.path.islink(path):
261 error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
262 (name, package_qa_clean_path(path,d))
263 sane = package_qa_handle_error(0, error_msg, name, path, d)
265 if not "-dbg" in name:
267 error_msg = "non debug package contains .debug directory: %s path %s" % \
268 (name, package_qa_clean_path(path,d))
269 sane = package_qa_handle_error(3, error_msg, name, path, d)
273 def package_qa_check_perm(path,name,d, elf):
275 Check the permission of files
280 def package_qa_check_arch(path,name,d, elf):
282 Check if archs are compatible
289 target_os = bb.data.getVar('TARGET_OS', d, True)
290 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
292 # FIXME: Cross package confuse this check, so just skip them
293 for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
294 if bb.data.inherits_class(s, d):
297 # avoid following links to /usr/bin (e.g. on udev builds)
298 # we will check the files pointed to anyway...
299 if os.path.islink(path):
302 #if this will throw an exception, then fix the dict above
303 (machine, osabi, abiversion, littleendian, bits32) \
304 = package_qa_get_machine_dict()[target_os][target_arch]
306 # Check the architecture and endiannes of the binary
307 if not machine == elf.machine():
308 error_msg = "Architecture did not match (%d to %d) on %s" % \
309 (machine, elf.machine(), package_qa_clean_path(path,d))
310 sane = package_qa_handle_error(4, error_msg, name, path, d)
311 elif not littleendian == elf.isLittleEndian():
312 error_msg = "Endiannes did not match (%d to %d) on %s" % \
313 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
314 sane = package_qa_handle_error(4, error_msg, name, path, d)
318 def package_qa_check_desktop(path, name, d, elf):
320 Run all desktop files through desktop-file-validate.
324 if path.endswith(".desktop"):
325 output = os.popen("desktop-file-validate %s" % path)
326 # This only produces output on errors
328 sane = package_qa_handle_error(7, l.strip(), name, path, d)
332 def package_qa_hash_style(path, name, d, elf):
334 Check if the binary has the right hash style...
341 if os.path.islink(path):
344 gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
346 gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
350 objdump = bb.data.getVar('OBJDUMP', d, True)
351 env_path = bb.data.getVar('PATH', d, True)
355 # A bit hacky. We do not know if path is an elf binary or not
356 # we will search for 'NEEDED' or 'INIT' as this should be printed...
357 # and come before the HASH section (guess!!!) and works on split out
359 for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
360 if "NEEDED" in line or "INIT" in line:
363 if "GNU_HASH" in line:
365 if "[mips32]" in line or "[mips64]" in line:
369 error_msg = "No GNU_HASH in the elf binary: '%s'" % path
370 return package_qa_handle_error(9, error_msg, name, path, d)
374 def package_qa_check_staged(path,d):
376 Check staged la and pc files for sanity
377 -e.g. installed being false
379 As this is run after every stage we should be able
380 to find the one responsible for the errors easily even
381 if we look at every .pc and .la file
386 tmpdir = bb.data.getVar('TMPDIR', d, True)
387 workdir = os.path.join(tmpdir, "work")
389 installed = "installed=yes"
390 iscrossnative = False
391 pkgconfigcheck = tmpdir
392 for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
393 if bb.data.inherits_class(s, d):
394 pkgconfigcheck = workdir
397 # find all .la and .pc files
399 # and check for stuff that looks wrong
400 for root, dirs, files in os.walk(path):
402 path = os.path.join(root,file)
403 if file[-2:] == "la":
404 file_content = open(path).read()
405 # Don't check installed status for native/cross packages
406 if not iscrossnative:
407 if installed in file_content:
408 error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
409 sane = package_qa_handle_error(5, error_msg, "staging", path, d)
410 if workdir in file_content:
411 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
412 sane = package_qa_handle_error(8, error_msg, "staging", path, d)
413 elif file[-2:] == "pc":
414 file_content = open(path).read()
415 if pkgconfigcheck in file_content:
416 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
417 #sane = package_qa_handle_error(6, error_msg, "staging", path, d)
421 # Walk over all files in a directory and call func
422 def package_qa_walk(path, funcs, package,d):
426 #if this will throw an exception, then fix the dict above
427 target_os = bb.data.getVar('TARGET_OS', d, True)
428 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
429 (machine, osabi, abiversion, littleendian, bits32) \
430 = package_qa_get_machine_dict()[target_os][target_arch]
432 for root, dirs, files in os.walk(path):
434 path = os.path.join(root,file)
435 elf = package_qa_get_elf(path, bits32)
441 if not func(path, package,d, elf):
446 def package_qa_check_rdepends(pkg, workdir, d):
449 if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
450 # Copied from package_ipk.bbclass
451 # boiler plate to update the data
452 localdata = bb.data.createCopy(d)
453 root = "%s/install/%s" % (workdir, pkg)
455 bb.data.setVar('ROOT', '', localdata)
456 bb.data.setVar('ROOT_%s' % pkg, root, localdata)
457 pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
460 bb.data.setVar('PKG', pkgname, localdata)
462 overrides = bb.data.getVar('OVERRIDES', localdata)
464 raise bb.build.FuncFailed('OVERRIDES not defined')
465 overrides = bb.data.expand(overrides, localdata)
466 bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
468 bb.data.update_data(localdata)
470 # Now check the RDEPENDS
471 rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
474 # Now do the sanity check!!!
475 for rdepend in rdepends:
476 if "-dbg" in rdepend:
477 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
478 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
482 # The PACKAGE FUNC to scan each package
483 python do_package_qa () {
485 bb.debug(2, "DO PACKAGE QA")
486 workdir = bb.data.getVar('WORKDIR', d, True)
487 packages = bb.data.getVar('PACKAGES',d, True)
489 # no packages should be scanned
493 checks = [package_qa_check_rpath, package_qa_check_devdbg,
494 package_qa_check_perm, package_qa_check_arch,
495 package_qa_check_desktop, package_qa_hash_style]
498 for package in packages.split():
499 if bb.data.getVar('INSANE_SKIP_' + package, d, True):
500 bb.note("package %s skipped" % package)
503 bb.debug(1, "Checking Package: %s" % package)
504 path = "%s/install/%s" % (workdir, package)
505 if not package_qa_walk(path, checks, package, d):
507 if not package_qa_check_rdepends(package, workdir, d):
508 rdepends_sane = False
510 if not walk_sane or not rdepends_sane:
511 bb.fatal("QA run found fatal errors. Please consider fixing them.")
512 bb.debug(2, "DONE with PACKAGE QA")
516 # The Staging Func, to check all staging
517 addtask qa_staging after do_populate_staging before do_build
518 python do_qa_staging() {
519 bb.debug(2, "QA checking staging")
521 if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
522 bb.fatal("QA staging was broken by the package built above")
525 # Check broken config.log files
526 addtask qa_configure after do_configure before do_compile
527 python do_qa_configure() {
528 bb.debug(1, "Checking sanity of the config.log file")
530 for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
531 statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
532 os.path.join(root,"config.log")
533 if "config.log" in files:
534 if os.system(statement) == 0:
535 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
536 Rerun configure task after fixing this. The path was '%s'""" % root)