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, 0, 0, True, True),
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),
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 "mips": ( 8, 0, 0, False, True),
65 "mipsel": ( 8, 0, 0, True, True),
66 "avr32": (6317, 0, 0, False, True),
67 "sh4": (42, 0, 0, True, True),
71 "bfin": ( 106, 0, 0, True, True),
74 "arm" : (40, 0, 0, True, True),
75 "armeb" : (40, 0, 0, False, True),
77 "linux-uclibceabi" : {
78 "arm" : (40, 0, 0, True, True),
79 "armeb" : (40, 0, 0, False, True),
82 "powerpc": (20, 0, 0, False, True),
85 "powerpc": (20, 0, 0, False, True),
90 # factory for a class, embedded in a method
91 def package_qa_get_elf(path, bits32):
101 # possible values for EI_CLASS
106 # possible value for EI_VERSION
109 # possible values for EI_DATA
114 def my_assert(self, expectation, result):
115 if not expectation == result:
116 #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
117 raise Exception("This does not work as expected")
119 def __init__(self, name):
123 self.file = file(self.name, "r")
124 self.data = self.file.read(ELFFile.EI_NIDENT+4)
126 self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
127 self.my_assert(self.data[0], chr(0x7f) )
128 self.my_assert(self.data[1], 'E')
129 self.my_assert(self.data[2], 'L')
130 self.my_assert(self.data[3], 'F')
132 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
134 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
135 self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
137 self.sex = self.data[ELFFile.EI_DATA]
138 if self.sex == chr(ELFFile.ELFDATANONE):
139 raise Exception("self.sex == ELFDATANONE")
140 elif self.sex == chr(ELFFile.ELFDATA2LSB):
142 elif self.sex == chr(ELFFile.ELFDATA2MSB):
145 raise Exception("Unknown self.sex")
148 return ord(self.data[ELFFile.EI_OSABI])
150 def abiVersion(self):
151 return ord(self.data[ELFFile.EI_ABIVERSION])
153 def isLittleEndian(self):
154 return self.sex == "<"
156 def isBigEngian(self):
157 return self.sex == ">"
161 We know the sex stored in self.sex and we
165 (a,) = struct.unpack(self.sex+"H", self.data[18:20])
171 # Known Error classes
172 # 0 - non dev contains .so
173 # 1 - package contains a dangerous RPATH
174 # 2 - package depends on debug package
175 # 3 - non dbg contains .so
176 # 4 - wrong architecture
177 # 5 - .la contains installed=yes or reference to the workdir
178 # 6 - .pc contains reference to /usr/include or workdir
179 # 7 - the desktop file is not valid
180 # 8 - .la contains reference to the workdir
181 # 9 - LDFLAGS ignored
183 def package_qa_clean_path(path,d):
184 """ Remove the common prefix from the path. In this case it is the TMPDIR"""
185 return path.replace(bb.data.getVar('TMPDIR',d,True),"")
187 def package_qa_make_fatal_error(error_class, name, path,d):
189 decide if an error is fatal
191 TODO: Load a whitelist of known errors
193 return not error_class in [0, 5, 7]
195 def package_qa_write_error(error_class, name, path, d):
199 if not bb.data.getVar('QA_LOG', d):
200 bb.note("a QA error occured but will not be logged because QA_LOG is not set")
204 "non dev contains .so",
205 "package contains RPATH",
206 "package depends on debug package",
207 "non dbg contains .debug",
208 "wrong architecture",
209 "evil hides inside the .la",
210 "evil hides inside the .pc",
211 "the desktop file is not valid",
212 ".la contains reference to the workdir",
216 log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
217 f = file( log_path, "a+")
218 print >> f, "%s, %s, %s" % \
219 (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
222 def package_qa_handle_error(error_class, error_msg, name, path, d):
223 bb.error("QA Issue with %s: %s" % (name, error_msg))
224 package_qa_write_error(error_class, name, path, d)
225 return not package_qa_make_fatal_error(error_class, name, path, d)
227 def package_qa_check_rpath(file,name,d, elf):
229 Check for dangerous RPATHs
236 scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
237 bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
238 bad_dir_test = bb.data.getVar('TMPDIR', d, True)
239 if not os.path.exists(scanelf):
240 bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
242 if not bad_dir in bb.data.getVar('WORKDIR', d, True):
243 bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
245 output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
246 txt = output.readline().split()
249 error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
250 sane = package_qa_handle_error(1, error_msg, name, file, d)
254 def package_qa_check_dev(path, name,d, elf):
256 Check for ".so" library symlinks in non-dev packages
261 # SDK packages are special.
262 for s in ['sdk', 'canadian-sdk']:
263 if bb.data.inherits_class(s, d):
266 if not name.endswith("-dev") and path.endswith(".so") and os.path.islink(path):
267 error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
268 (name, package_qa_clean_path(path,d))
269 sane = package_qa_handle_error(0, error_msg, name, path, d)
273 def package_qa_check_dbg(path, name,d, elf):
275 Check for ".debug" files or directories outside of the dbg package
280 if not "-dbg" in name:
281 if '.debug' in path.split(os.path.sep):
282 error_msg = "non debug package contains .debug directory: %s path %s" % \
283 (name, package_qa_clean_path(path,d))
284 sane = package_qa_handle_error(3, error_msg, name, path, d)
288 def package_qa_check_perm(path,name,d, elf):
290 Check the permission of files
295 def package_qa_check_arch(path,name,d, elf):
297 Check if archs are compatible
303 target_os = bb.data.getVar('TARGET_OS', d, True)
304 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
306 # FIXME: Cross package confuse this check, so just skip them
307 for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
308 if bb.data.inherits_class(s, d):
311 # avoid following links to /usr/bin (e.g. on udev builds)
312 # we will check the files pointed to anyway...
313 if os.path.islink(path):
316 #if this will throw an exception, then fix the dict above
317 (machine, osabi, abiversion, littleendian, bits32) \
318 = package_qa_get_machine_dict()[target_os][target_arch]
320 # Check the architecture and endiannes of the binary
321 if not machine == elf.machine():
322 error_msg = "Architecture did not match (%d to %d) on %s" % \
323 (machine, elf.machine(), package_qa_clean_path(path,d))
324 sane = package_qa_handle_error(4, error_msg, name, path, d)
325 elif not littleendian == elf.isLittleEndian():
326 error_msg = "Endiannes did not match (%d to %d) on %s" % \
327 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
328 sane = package_qa_handle_error(4, error_msg, name, path, d)
332 def package_qa_check_desktop(path, name, d, elf):
334 Run all desktop files through desktop-file-validate.
337 if path.endswith(".desktop"):
338 output = os.popen("desktop-file-validate %s" % path)
339 # This only produces output on errors
341 sane = package_qa_handle_error(7, l.strip(), name, path, d)
345 def package_qa_hash_style(path, name, d, elf):
347 Check if the binary has the right hash style...
353 if os.path.islink(path):
356 gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
358 gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
362 objdump = bb.data.getVar('OBJDUMP', d, True)
363 env_path = bb.data.getVar('PATH', d, True)
367 # A bit hacky. We do not know if path is an elf binary or not
368 # we will search for 'NEEDED' or 'INIT' as this should be printed...
369 # and come before the HASH section (guess!!!) and works on split out
371 for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
372 if "NEEDED" in line or "INIT" in line:
375 if "GNU_HASH" in line:
377 if "[mips32]" in line or "[mips64]" in line:
381 error_msg = "No GNU_HASH in the elf binary: '%s'" % path
382 return package_qa_handle_error(9, error_msg, name, path, d)
386 def package_qa_check_staged(path,d):
388 Check staged la and pc files for sanity
389 -e.g. installed being false
391 As this is run after every stage we should be able
392 to find the one responsible for the errors easily even
393 if we look at every .pc and .la file
397 tmpdir = bb.data.getVar('TMPDIR', d, True)
398 workdir = os.path.join(tmpdir, "work")
400 installed = "installed=yes"
401 iscrossnative = False
402 pkgconfigcheck = tmpdir
403 for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
404 if bb.data.inherits_class(s, d):
405 pkgconfigcheck = workdir
408 # find all .la and .pc files
410 # and check for stuff that looks wrong
411 for root, dirs, files in os.walk(path):
413 path = os.path.join(root,file)
414 if file.endswith(".la"):
415 file_content = open(path).read()
416 # Don't check installed status for native/cross packages
417 if not iscrossnative:
418 if installed in file_content:
419 error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
420 sane = package_qa_handle_error(5, error_msg, "staging", path, d)
421 if workdir in file_content:
422 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
423 sane = package_qa_handle_error(8, error_msg, "staging", path, d)
424 elif file.endswith(".pc"):
425 file_content = open(path).read()
426 if pkgconfigcheck in file_content:
427 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
428 sane = package_qa_handle_error(6, error_msg, "staging", path, d)
432 # Walk over all files in a directory and call func
433 def package_qa_walk(path, funcs, package,d):
436 #if this will throw an exception, then fix the dict above
437 target_os = bb.data.getVar('TARGET_OS', d, True)
438 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
439 (machine, osabi, abiversion, littleendian, bits32) \
440 = package_qa_get_machine_dict()[target_os][target_arch]
442 for root, dirs, files in os.walk(path):
444 path = os.path.join(root,file)
445 elf = package_qa_get_elf(path, bits32)
451 if not func(path, package,d, elf):
456 def package_qa_check_rdepends(pkg, pkgdest, d):
458 if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
459 # Copied from package_ipk.bbclass
460 # boiler plate to update the data
461 localdata = bb.data.createCopy(d)
462 root = "%s/%s" % (pkgdest, pkg)
464 bb.data.setVar('ROOT', '', localdata)
465 bb.data.setVar('ROOT_%s' % pkg, root, localdata)
466 pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
469 bb.data.setVar('PKG', pkgname, localdata)
471 overrides = bb.data.getVar('OVERRIDES', localdata)
473 raise bb.build.FuncFailed('OVERRIDES not defined')
474 overrides = bb.data.expand(overrides, localdata)
475 bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
477 bb.data.update_data(localdata)
479 # Now check the RDEPENDS
480 rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
483 # Now do the sanity check!!!
484 for rdepend in rdepends:
485 if "-dbg" in rdepend:
486 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
487 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
491 # The PACKAGE FUNC to scan each package
492 python do_package_qa () {
493 bb.debug(2, "DO PACKAGE QA")
494 pkgdest = bb.data.getVar('PKGDEST', d, True)
495 packages = bb.data.getVar('PACKAGES',d, True)
497 # no packages should be scanned
501 checks = [package_qa_check_rpath, package_qa_check_dev,
502 package_qa_check_perm, package_qa_check_arch,
503 package_qa_check_desktop, package_qa_hash_style,
504 package_qa_check_dbg]
507 for package in packages.split():
508 if bb.data.getVar('INSANE_SKIP_' + package, d, True):
509 bb.note("package %s skipped" % package)
512 bb.debug(1, "Checking Package: %s" % package)
513 path = "%s/%s" % (pkgdest, package)
514 if not package_qa_walk(path, checks, package, d):
516 if not package_qa_check_rdepends(package, pkgdest, d):
517 rdepends_sane = False
519 if not walk_sane or not rdepends_sane:
520 bb.fatal("QA run found fatal errors. Please consider fixing them.")
521 bb.debug(2, "DONE with PACKAGE QA")
525 # The Staging Func, to check all staging
526 addtask qa_staging after do_populate_staging before do_build
527 python do_qa_staging() {
528 bb.debug(2, "QA checking staging")
530 if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
531 bb.fatal("QA staging was broken by the package built above")
534 # Check broken config.log files
535 addtask qa_configure after do_configure before do_compile
536 python do_qa_configure() {
537 bb.debug(1, "Checking sanity of the config.log file")
538 for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
539 statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
540 os.path.join(root,"config.log")
541 if "config.log" in files:
542 if os.system(statement) == 0:
543 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
544 Rerun configure task after fixing this. The path was '%s'""" % root)