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 "x86_64": ( 62, 0, 0, True, False),
65 "mips": ( 8, 0, 0, False, True),
66 "mipsel": ( 8, 0, 0, True, True),
67 "avr32": (6317, 0, 0, False, True),
68 "sh4": (42, 0, 0, True, True),
72 "bfin": ( 106, 0, 0, True, True),
75 "arm" : (40, 0, 0, True, True),
76 "armeb" : (40, 0, 0, False, True),
78 "linux-uclibceabi" : {
79 "arm" : (40, 0, 0, True, True),
80 "armeb" : (40, 0, 0, False, True),
83 "powerpc": (20, 0, 0, False, True),
86 "powerpc": (20, 0, 0, False, True),
91 # factory for a class, embedded in a method
92 def package_qa_get_elf(path, bits32):
102 # possible values for EI_CLASS
107 # possible value for EI_VERSION
110 # possible values for EI_DATA
115 def my_assert(self, expectation, result):
116 if not expectation == result:
117 #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
118 raise Exception("This does not work as expected")
120 def __init__(self, name):
124 self.file = file(self.name, "r")
125 self.data = self.file.read(ELFFile.EI_NIDENT+4)
127 self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
128 self.my_assert(self.data[0], chr(0x7f) )
129 self.my_assert(self.data[1], 'E')
130 self.my_assert(self.data[2], 'L')
131 self.my_assert(self.data[3], 'F')
133 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
135 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
136 self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
138 self.sex = self.data[ELFFile.EI_DATA]
139 if self.sex == chr(ELFFile.ELFDATANONE):
140 raise Exception("self.sex == ELFDATANONE")
141 elif self.sex == chr(ELFFile.ELFDATA2LSB):
143 elif self.sex == chr(ELFFile.ELFDATA2MSB):
146 raise Exception("Unknown self.sex")
149 return ord(self.data[ELFFile.EI_OSABI])
151 def abiVersion(self):
152 return ord(self.data[ELFFile.EI_ABIVERSION])
154 def isLittleEndian(self):
155 return self.sex == "<"
157 def isBigEngian(self):
158 return self.sex == ">"
162 We know the sex stored in self.sex and we
166 (a,) = struct.unpack(self.sex+"H", self.data[18:20])
172 # Known Error classes
173 # 0 - non dev contains .so
174 # 1 - package contains a dangerous RPATH
175 # 2 - package depends on debug package
176 # 3 - non dbg contains .so
177 # 4 - wrong architecture
178 # 5 - .la contains installed=yes or reference to the workdir
179 # 6 - .pc contains reference to /usr/include or workdir
180 # 7 - the desktop file is not valid
181 # 8 - .la contains reference to the workdir
182 # 9 - LDFLAGS ignored
184 def package_qa_clean_path(path,d):
185 """ Remove the common prefix from the path. In this case it is the TMPDIR"""
186 return path.replace(bb.data.getVar('TMPDIR',d,True),"")
188 def package_qa_make_fatal_error(error_class, name, path,d):
190 decide if an error is fatal
192 TODO: Load a whitelist of known errors
194 return not error_class in [0, 5, 7]
196 def package_qa_write_error(error_class, name, path, d):
200 if not bb.data.getVar('QA_LOG', d):
201 bb.note("a QA error occured but will not be logged because QA_LOG is not set")
205 "non dev contains .so",
206 "package contains RPATH",
207 "package depends on debug package",
208 "non dbg contains .debug",
209 "wrong architecture",
210 "evil hides inside the .la",
211 "evil hides inside the .pc",
212 "the desktop file is not valid",
213 ".la contains reference to the workdir",
217 log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
218 f = file( log_path, "a+")
219 print >> f, "%s, %s, %s" % \
220 (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
223 def package_qa_handle_error(error_class, error_msg, name, path, d):
224 bb.error("QA Issue with %s: %s" % (name, error_msg))
225 package_qa_write_error(error_class, name, path, d)
226 return not package_qa_make_fatal_error(error_class, name, path, d)
228 def package_qa_check_rpath(file,name,d, elf):
230 Check for dangerous RPATHs
237 scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
238 bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
239 bad_dir_test = bb.data.getVar('TMPDIR', d, True)
240 if not os.path.exists(scanelf):
241 bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
243 if not bad_dir in bb.data.getVar('WORKDIR', d, True):
244 bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
246 output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
247 txt = output.readline().split()
250 error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
251 sane = package_qa_handle_error(1, error_msg, name, file, d)
255 def package_qa_check_dev(path, name,d, elf):
257 Check for ".so" library symlinks in non-dev packages
262 # SDK packages are special.
263 for s in ['sdk', 'canadian-sdk']:
264 if bb.data.inherits_class(s, d):
267 if not name.endswith("-dev") and path.endswith(".so") and os.path.islink(path):
268 error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
269 (name, package_qa_clean_path(path,d))
270 sane = package_qa_handle_error(0, error_msg, name, path, d)
274 def package_qa_check_dbg(path, name,d, elf):
276 Check for ".debug" files or directories outside of the dbg package
281 if not "-dbg" in name:
282 if '.debug' in path.split(os.path.sep):
283 error_msg = "non debug package contains .debug directory: %s path %s" % \
284 (name, package_qa_clean_path(path,d))
285 sane = package_qa_handle_error(3, error_msg, name, path, d)
289 def package_qa_check_perm(path,name,d, elf):
291 Check the permission of files
296 def package_qa_check_arch(path,name,d, elf):
298 Check if archs are compatible
304 target_os = bb.data.getVar('TARGET_OS', d, True)
305 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
307 # FIXME: Cross package confuse this check, so just skip them
308 for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
309 if bb.data.inherits_class(s, d):
312 # avoid following links to /usr/bin (e.g. on udev builds)
313 # we will check the files pointed to anyway...
314 if os.path.islink(path):
317 #if this will throw an exception, then fix the dict above
318 (machine, osabi, abiversion, littleendian, bits32) \
319 = package_qa_get_machine_dict()[target_os][target_arch]
321 # Check the architecture and endiannes of the binary
322 if not machine == elf.machine():
323 error_msg = "Architecture did not match (%d to %d) on %s" % \
324 (machine, elf.machine(), package_qa_clean_path(path,d))
325 sane = package_qa_handle_error(4, error_msg, name, path, d)
326 elif not littleendian == elf.isLittleEndian():
327 error_msg = "Endiannes did not match (%d to %d) on %s" % \
328 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
329 sane = package_qa_handle_error(4, error_msg, name, path, d)
333 def package_qa_check_desktop(path, name, d, elf):
335 Run all desktop files through desktop-file-validate.
338 if path.endswith(".desktop"):
339 output = os.popen("desktop-file-validate %s" % path)
340 # This only produces output on errors
342 sane = package_qa_handle_error(7, l.strip(), name, path, d)
346 def package_qa_hash_style(path, name, d, elf):
348 Check if the binary has the right hash style...
354 if os.path.islink(path):
357 gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
359 gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
363 objdump = bb.data.getVar('OBJDUMP', d, True)
364 env_path = bb.data.getVar('PATH', d, True)
368 # A bit hacky. We do not know if path is an elf binary or not
369 # we will search for 'NEEDED' or 'INIT' as this should be printed...
370 # and come before the HASH section (guess!!!) and works on split out
372 for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
373 if "NEEDED" in line or "INIT" in line:
376 if "GNU_HASH" in line:
378 if "[mips32]" in line or "[mips64]" in line:
382 error_msg = "No GNU_HASH in the elf binary: '%s'" % path
383 return package_qa_handle_error(9, error_msg, name, path, d)
387 def package_qa_check_staged(path,d):
389 Check staged la and pc files for sanity
390 -e.g. installed being false
392 As this is run after every stage we should be able
393 to find the one responsible for the errors easily even
394 if we look at every .pc and .la file
398 tmpdir = bb.data.getVar('TMPDIR', d, True)
399 workdir = os.path.join(tmpdir, "work")
401 installed = "installed=yes"
402 iscrossnative = False
403 pkgconfigcheck = tmpdir
404 for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
405 if bb.data.inherits_class(s, d):
406 pkgconfigcheck = workdir
409 # find all .la and .pc files
411 # and check for stuff that looks wrong
412 for root, dirs, files in os.walk(path):
414 path = os.path.join(root,file)
415 if file.endswith(".la"):
416 file_content = open(path).read()
417 # Don't check installed status for native/cross packages
418 if not iscrossnative:
419 if installed in file_content:
420 error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
421 sane = package_qa_handle_error(5, error_msg, "staging", path, d)
422 if workdir in file_content:
423 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
424 sane = package_qa_handle_error(8, error_msg, "staging", path, d)
425 elif file.endswith(".pc"):
426 file_content = open(path).read()
427 if pkgconfigcheck in file_content:
428 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
429 sane = package_qa_handle_error(6, error_msg, "staging", path, d)
433 # Walk over all files in a directory and call func
434 def package_qa_walk(path, funcs, package,d):
437 #if this will throw an exception, then fix the dict above
438 target_os = bb.data.getVar('TARGET_OS', d, True)
439 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
440 (machine, osabi, abiversion, littleendian, bits32) \
441 = package_qa_get_machine_dict()[target_os][target_arch]
443 for root, dirs, files in os.walk(path):
445 path = os.path.join(root,file)
446 elf = package_qa_get_elf(path, bits32)
452 if not func(path, package,d, elf):
457 def package_qa_check_rdepends(pkg, pkgdest, d):
459 if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
460 # Copied from package_ipk.bbclass
461 # boiler plate to update the data
462 localdata = bb.data.createCopy(d)
463 root = "%s/%s" % (pkgdest, pkg)
465 bb.data.setVar('ROOT', '', localdata)
466 bb.data.setVar('ROOT_%s' % pkg, root, localdata)
467 pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
470 bb.data.setVar('PKG', pkgname, localdata)
472 overrides = bb.data.getVar('OVERRIDES', localdata)
474 raise bb.build.FuncFailed('OVERRIDES not defined')
475 overrides = bb.data.expand(overrides, localdata)
476 bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
478 bb.data.update_data(localdata)
480 # Now check the RDEPENDS
481 rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
484 # Now do the sanity check!!!
485 for rdepend in rdepends:
486 if "-dbg" in rdepend:
487 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
488 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
492 # The PACKAGE FUNC to scan each package
493 python do_package_qa () {
494 bb.debug(2, "DO PACKAGE QA")
495 pkgdest = bb.data.getVar('PKGDEST', d, True)
496 packages = bb.data.getVar('PACKAGES',d, True)
498 # no packages should be scanned
502 checks = [package_qa_check_rpath, package_qa_check_dev,
503 package_qa_check_perm, package_qa_check_arch,
504 package_qa_check_desktop, package_qa_hash_style,
505 package_qa_check_dbg]
508 for package in packages.split():
509 if bb.data.getVar('INSANE_SKIP_' + package, d, True):
510 bb.note("package %s skipped" % package)
513 bb.debug(1, "Checking Package: %s" % package)
514 path = "%s/%s" % (pkgdest, package)
515 if not package_qa_walk(path, checks, package, d):
517 if not package_qa_check_rdepends(package, pkgdest, d):
518 rdepends_sane = False
520 if not walk_sane or not rdepends_sane:
521 bb.fatal("QA run found fatal errors. Please consider fixing them.")
522 bb.debug(2, "DONE with PACKAGE QA")
526 # The Staging Func, to check all staging
527 addtask qa_staging after do_populate_staging before do_build
528 python do_qa_staging() {
529 bb.debug(2, "QA checking staging")
531 if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
532 bb.fatal("QA staging was broken by the package built above")
535 # Check broken config.log files
536 addtask qa_configure after do_configure before do_compile
537 python do_qa_configure() {
538 bb.debug(1, "Checking sanity of the config.log file")
539 for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
540 statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
541 os.path.join(root,"config.log")
542 if "config.log" in files:
543 if os.system(statement) == 0:
544 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
545 Rerun configure task after fixing this. The path was '%s'""" % root)