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
175 def package_qa_clean_path(path,d):
176 """ Remove the common prefix from the path. In this case it is the TMPDIR"""
178 return path.replace(bb.data.getVar('TMPDIR',d,True),"")
180 def package_qa_make_fatal_error(error_class, name, path,d):
182 decide if an error is fatal
184 TODO: Load a whitelist of known errors
186 return not error_class in [0, 5, 7, 9]
188 def package_qa_write_error(error_class, name, path, d):
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")
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",
210 log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
211 f = file( log_path, "a+")
212 print >> f, "%s, %s, %s" % \
213 (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
216 def package_qa_handle_error(error_class, error_msg, name, path, d):
218 bb.error("QA Issue: %s" % error_msg)
219 package_qa_write_error(error_class, name, path, d)
220 return not package_qa_make_fatal_error(error_class, name, path, d)
222 def package_qa_check_rpath(file,name,d, elf):
224 Check for dangerous RPATHs
231 scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
232 bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
233 bad_dir_test = bb.data.getVar('TMPDIR', d, True)
234 if not os.path.exists(scanelf):
235 bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
237 if not bad_dir in bb.data.getVar('WORKDIR', d, True):
238 bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
240 output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
241 txt = output.readline().split()
244 error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
245 sane = package_qa_handle_error(1, error_msg, name, file, d)
249 def package_qa_check_devdbg(path, name,d, elf):
251 Check for debug remains inside the binary or
252 non dev packages containing
258 if not "-dev" in name:
259 if path[-3:] == ".so" and os.path.islink(path):
260 error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
261 (name, package_qa_clean_path(path,d))
262 sane = package_qa_handle_error(0, error_msg, name, path, d)
264 if not "-dbg" in name:
266 error_msg = "non debug package contains .debug directory: %s path %s" % \
267 (name, package_qa_clean_path(path,d))
268 sane = package_qa_handle_error(3, error_msg, name, path, d)
272 def package_qa_check_perm(path,name,d, elf):
274 Check the permission of files
279 def package_qa_check_arch(path,name,d, elf):
281 Check if archs are compatible
288 target_os = bb.data.getVar('TARGET_OS', d, True)
289 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
291 # FIXME: Cross package confuse this check, so just skip them
292 for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
293 if bb.data.inherits_class(s, d):
296 # avoid following links to /usr/bin (e.g. on udev builds)
297 # we will check the files pointed to anyway...
298 if os.path.islink(path):
301 #if this will throw an exception, then fix the dict above
302 (machine, osabi, abiversion, littleendian, bits32) \
303 = package_qa_get_machine_dict()[target_os][target_arch]
305 # Check the architecture and endiannes of the binary
306 if not machine == elf.machine():
307 error_msg = "Architecture did not match (%d to %d) on %s" % \
308 (machine, elf.machine(), package_qa_clean_path(path,d))
309 sane = package_qa_handle_error(4, error_msg, name, path, d)
310 elif not littleendian == elf.isLittleEndian():
311 error_msg = "Endiannes did not match (%d to %d) on %s" % \
312 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
313 sane = package_qa_handle_error(4, error_msg, name, path, d)
317 def package_qa_check_desktop(path, name, d, elf):
319 Run all desktop files through desktop-file-validate.
323 if path.endswith(".desktop"):
324 output = os.popen("desktop-file-validate %s" % path)
325 # This only produces output on errors
327 sane = package_qa_handle_error(7, l.strip(), name, path, d)
331 def package_qa_hash_style(path, name, d, elf):
333 Check if the binary has the right hash style...
340 if os.path.islink(path):
343 gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
345 gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
347 objdump = bb.data.getVar('OBJDUMP', d, True)
348 env_path = bb.data.getVar('PATH', d, True)
352 # A bit hacky. We do not know if path is an elf binary or not
353 # we will search for 'NEEDED' or 'INIT' as this should be printed...
354 # and come before the HASH section (guess!!!) and works on split out
356 for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
357 if "NEEDED" in line or "INIT" in line:
360 if "GNU_HASH" in line:
362 if "[mips32]" in line or "[mips64]" in line:
366 error_msg = "No GNU_HASH in the elf binary: '%s'" % path
367 return package_qa_handle_error(9, error_msg, name, path, d)
371 def package_qa_check_staged(path,d):
373 Check staged la and pc files for sanity
374 -e.g. installed being false
376 As this is run after every stage we should be able
377 to find the one responsible for the errors easily even
378 if we look at every .pc and .la file
383 tmpdir = bb.data.getVar('TMPDIR', d, True)
384 workdir = os.path.join(tmpdir, "work")
386 installed = "installed=yes"
387 iscrossnative = False
388 pkgconfigcheck = tmpdir
389 for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
390 if bb.data.inherits_class(s, d):
391 pkgconfigcheck = workdir
394 # find all .la and .pc files
396 # and check for stuff that looks wrong
397 for root, dirs, files in os.walk(path):
399 path = os.path.join(root,file)
400 if file[-2:] == "la":
401 file_content = open(path).read()
402 # Don't check installed status for native/cross packages
403 if not iscrossnative:
404 if installed in file_content:
405 error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
406 sane = package_qa_handle_error(5, error_msg, "staging", path, d)
407 if workdir in file_content:
408 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
409 sane = package_qa_handle_error(8, error_msg, "staging", path, d)
410 elif file[-2:] == "pc":
411 file_content = open(path).read()
412 if pkgconfigcheck in file_content:
413 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
414 sane = package_qa_handle_error(6, error_msg, "staging", path, d)
418 # Walk over all files in a directory and call func
419 def package_qa_walk(path, funcs, package,d):
423 #if this will throw an exception, then fix the dict above
424 target_os = bb.data.getVar('TARGET_OS', d, True)
425 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
426 (machine, osabi, abiversion, littleendian, bits32) \
427 = package_qa_get_machine_dict()[target_os][target_arch]
429 for root, dirs, files in os.walk(path):
431 path = os.path.join(root,file)
432 elf = package_qa_get_elf(path, bits32)
438 if not func(path, package,d, elf):
443 def package_qa_check_rdepends(pkg, workdir, d):
446 if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
447 # Copied from package_ipk.bbclass
448 # boiler plate to update the data
449 localdata = bb.data.createCopy(d)
450 root = "%s/install/%s" % (workdir, pkg)
452 bb.data.setVar('ROOT', '', localdata)
453 bb.data.setVar('ROOT_%s' % pkg, root, localdata)
454 pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
457 bb.data.setVar('PKG', pkgname, localdata)
459 overrides = bb.data.getVar('OVERRIDES', localdata)
461 raise bb.build.FuncFailed('OVERRIDES not defined')
462 overrides = bb.data.expand(overrides, localdata)
463 bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
465 bb.data.update_data(localdata)
467 # Now check the RDEPENDS
468 rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
471 # Now do the sanity check!!!
472 for rdepend in rdepends:
473 if "-dbg" in rdepend:
474 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
475 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
479 # The PACKAGE FUNC to scan each package
480 python do_package_qa () {
482 bb.note("DO PACKAGE QA")
483 workdir = bb.data.getVar('WORKDIR', d, True)
484 packages = bb.data.getVar('PACKAGES',d, True)
486 # no packages should be scanned
490 checks = [package_qa_check_rpath, package_qa_check_devdbg,
491 package_qa_check_perm, package_qa_check_arch,
492 package_qa_check_desktop, package_qa_hash_style]
495 for package in packages.split():
496 if bb.data.getVar('INSANE_SKIP_' + package, d, True):
497 bb.note("Package: %s (skipped)" % package)
500 bb.note("Checking Package: %s" % package)
501 path = "%s/install/%s" % (workdir, package)
502 if not package_qa_walk(path, checks, package, d):
504 if not package_qa_check_rdepends(package, workdir, d):
505 rdepends_sane = False
507 if not walk_sane or not rdepends_sane:
508 bb.fatal("QA run found fatal errors. Please consider fixing them.")
509 bb.note("DONE with PACKAGE QA")
513 # The Staging Func, to check all staging
514 addtask qa_staging after do_populate_staging before do_build
515 python do_qa_staging() {
516 bb.note("QA checking staging")
518 if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
519 bb.fatal("QA staging was broken by the package built above")
522 # Check broken config.log files
523 addtask qa_configure after do_configure before do_compile
524 python do_qa_configure() {
525 bb.note("Checking sanity of the config.log file")
527 for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
528 statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
529 os.path.join(root,"config.log")
530 if "config.log" in files:
531 if os.system(statement) == 0:
532 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
533 Rerun configure task after fixing this. The path was '%s'""" % root)