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 "i386": ( 3, 0, 0, True, True),
41 "i486": ( 3, 0, 0, True, True),
42 "i586": ( 3, 0, 0, True, True),
43 "i686": ( 3, 0, 0, True, True),
44 "x86_64": ( 62, 0, 0, True, False),
45 "ia64": ( 50, 0, 0, True, False),
46 "alpha": (36902, 0, 0, True, False),
47 "hppa": ( 15, 3, 0, False, True),
48 "m68k": ( 4, 0, 0, False, True),
49 "mips": ( 8, 0, 0, False, True),
50 "mipsel": ( 8, 0, 0, True, True),
51 "mips64": ( 8, 0, 0, False, False),
52 "mips64el": ( 8, 0, 0, True, False),
53 "nios2": ( 113, 0, 0, True, True),
54 "powerpc": ( 20, 0, 0, False, True),
55 "powerpc64": ( 21, 0, 0, False, False),
56 "s390": ( 22, 0, 0, False, True),
57 "sh4": ( 42, 0, 0, True, True),
58 "sparc": ( 2, 0, 0, False, True),
61 "arm" : ( 40, 97, 0, True, True),
62 "armeb": ( 40, 97, 0, False, True),
63 "avr32": ( 6317, 0, 0, False, True),
64 "i386": ( 3, 0, 0, True, True),
65 "i486": ( 3, 0, 0, True, True),
66 "i586": ( 3, 0, 0, True, True),
67 "i686": ( 3, 0, 0, True, True),
68 "x86_64": ( 62, 0, 0, True, False),
69 "mips": ( 8, 0, 0, False, True),
70 "mipsel": ( 8, 0, 0, True, True),
71 "mips64": ( 8, 0, 0, False, False),
72 "mips64el": ( 8, 0, 0, True, False),
73 "nios2": ( 113, 0, 0, True, True),
74 "powerpc": ( 20, 0, 0, False, True),
75 "powerpc64": ( 21, 0, 0, False, False),
76 "sh4": ( 42, 0, 0, True, True),
79 "bfin": ( 106, 0, 0, True, True),
82 "arm" : ( 40, 0, 0, True, True),
83 "armeb" : ( 40, 0, 0, False, True),
85 "linux-uclibceabi" : {
86 "arm" : ( 40, 0, 0, True, True),
87 "armeb" : ( 40, 0, 0, False, True),
90 "powerpc": ( 20, 0, 0, False, True),
93 "powerpc": ( 20, 0, 0, False, True),
100 # 0 - non dev contains .so
101 # 1 - package contains a dangerous RPATH
102 # 2 - package depends on debug package
103 # 3 - non dbg contains .so
104 # 4 - wrong architecture
105 # 5 - .la contains installed=yes or reference to the workdir
106 # 6 - .pc contains reference to /usr/include or workdir
107 # 7 - the desktop file is not valid
108 # 8 - .la contains reference to the workdir
109 # 9 - LDFLAGS ignored
111 def package_qa_clean_path(path,d):
112 """ Remove the common prefix from the path. In this case it is the TMPDIR"""
113 return path.replace(bb.data.getVar('TMPDIR',d,True),"")
115 def package_qa_make_fatal_error(error_class, name, path,d):
117 decide if an error is fatal
119 TODO: Load a whitelist of known errors
121 return not error_class in [0, 1, 5, 7]
123 def package_qa_write_error(error_class, name, path, d):
127 if not bb.data.getVar('QA_LOG', d):
128 bb.note("a QA error occured but will not be logged because QA_LOG is not set")
132 "non dev contains .so",
133 "package contains RPATH (security issue!)",
134 "package depends on debug package",
135 "non dbg contains .debug",
136 "wrong architecture",
137 "evil hides inside the .la",
138 "evil hides inside the .pc",
139 "the desktop file is not valid",
140 ".la contains reference to the workdir",
144 log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
145 f = file( log_path, "a+")
146 print >> f, "%s, %s, %s" % \
147 (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
150 # Returns False is there was a fatal problem and True if we did not hit a fatal
152 def package_qa_handle_error(error_class, error_msg, name, path, d):
153 fatal = package_qa_make_fatal_error(error_class, name, path, d)
154 package_qa_write_error(error_class, name, path, d)
156 bb.error("QA Issue with %s: %s" % (name, error_msg))
158 bb.warn("QA Issue with %s: %s" % (name, error_msg))
161 def package_qa_check_rpath(file,name,d, elf):
163 Check for dangerous RPATHs
170 scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
171 bad_dirs = [bb.data.getVar('TMPDIR', d, True) + "/work", bb.data.getVar('STAGING_DIR_TARGET', d, True)]
172 bad_dir_test = bb.data.getVar('TMPDIR', d, True)
173 if not os.path.exists(scanelf):
174 bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
176 if not bad_dirs[0] in bb.data.getVar('WORKDIR', d, True):
177 bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
179 output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
180 txt = output.readline().split()
184 error_msg = "package %s contains bad RPATH %s in file %s, this is a security issue" % (name, line, file)
185 sane = package_qa_handle_error(1, error_msg, name, file, d)
189 def package_qa_check_dev(path, name,d, elf):
191 Check for ".so" library symlinks in non-dev packages
196 # SDK packages are special.
197 for s in ['sdk', 'canadian-sdk']:
198 if bb.data.inherits_class(s, d):
201 if not name.endswith("-dev") and path.endswith(".so") and os.path.islink(path):
202 error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
203 (name, package_qa_clean_path(path,d))
204 sane = package_qa_handle_error(0, error_msg, name, path, d)
208 def package_qa_check_dbg(path, name,d, elf):
210 Check for ".debug" files or directories outside of the dbg package
215 if not "-dbg" in name:
216 if '.debug' in path.split(os.path.sep):
217 error_msg = "non debug package contains .debug directory: %s path %s" % \
218 (name, package_qa_clean_path(path,d))
219 sane = package_qa_handle_error(3, error_msg, name, path, d)
223 def package_qa_check_perm(path,name,d, elf):
225 Check the permission of files
230 def package_qa_check_arch(path,name,d, elf):
232 Check if archs are compatible
238 target_os = bb.data.getVar('TARGET_OS', d, True)
239 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
241 # FIXME: Cross package confuse this check, so just skip them
242 for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
243 if bb.data.inherits_class(s, d):
246 # avoid following links to /usr/bin (e.g. on udev builds)
247 # we will check the files pointed to anyway...
248 if os.path.islink(path):
251 #if this will throw an exception, then fix the dict above
252 (machine, osabi, abiversion, littleendian, bits32) \
253 = package_qa_get_machine_dict()[target_os][target_arch]
255 # Check the architecture and endiannes of the binary
256 if not machine == elf.machine():
257 error_msg = "Architecture did not match (%d to %d) on %s" % \
258 (machine, elf.machine(), package_qa_clean_path(path,d))
259 sane = package_qa_handle_error(4, error_msg, name, path, d)
260 elif not littleendian == elf.isLittleEndian():
261 error_msg = "Endiannes did not match (%d to %d) on %s" % \
262 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
263 sane = package_qa_handle_error(4, error_msg, name, path, d)
267 def package_qa_check_desktop(path, name, d, elf):
269 Run all desktop files through desktop-file-validate.
272 env_path = bb.data.getVar('PATH', d, True)
274 if path.endswith(".desktop"):
275 output = os.popen("PATH=%s desktop-file-validate %s" % (env_path, path))
276 # This only produces output on errors
278 sane = package_qa_handle_error(7, l.strip(), name, path, d)
282 def package_qa_hash_style(path, name, d, elf):
284 Check if the binary has the right hash style...
290 if os.path.islink(path):
293 gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
295 gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
299 objdump = bb.data.getVar('OBJDUMP', d, True)
300 env_path = bb.data.getVar('PATH', d, True)
304 # A bit hacky. We do not know if path is an elf binary or not
305 # we will search for 'NEEDED' or 'INIT' as this should be printed...
306 # and come before the HASH section (guess!!!) and works on split out
308 for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
309 if "NEEDED" in line or "INIT" in line:
312 if "GNU_HASH" in line:
314 if "[mips32]" in line or "[mips64]" in line:
318 error_msg = "No GNU_HASH in the elf binary: '%s'" % path
319 return package_qa_handle_error(9, error_msg, name, path, d)
323 def package_qa_check_staged(path,d):
325 Check staged la and pc files for sanity
326 -e.g. installed being false
328 As this is run after every stage we should be able
329 to find the one responsible for the errors easily even
330 if we look at every .pc and .la file
334 tmpdir = bb.data.getVar('TMPDIR', d, True)
335 workdir = os.path.join(tmpdir, "work")
337 installed = "installed=yes"
338 iscrossnative = False
339 pkgconfigcheck = tmpdir
340 for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
341 if bb.data.inherits_class(s, d):
342 pkgconfigcheck = workdir
345 # Grab the lock, find all .la and .pc files, read the content and check for
346 # stuff that looks wrong
347 lf = bb.utils.lockfile(bb.data.expand("${STAGING_DIR}/staging.lock", d))
348 for root, dirs, files in os.walk(path):
350 path = os.path.join(root,file)
351 if file.endswith(".la"):
352 file_content = open(path).read()
353 # Don't check installed status for native/cross packages
354 if not iscrossnative and bb.data.getVar('LIBTOOL_HAS_SYSROOT', d, True) is "no":
355 if installed in file_content:
356 error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
357 sane = package_qa_handle_error(5, error_msg, "staging", path, d)
358 if workdir in file_content:
359 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
360 sane = package_qa_handle_error(8, error_msg, "staging", path, d)
361 elif file.endswith(".pc"):
362 file_content = open(path).read()
363 if pkgconfigcheck in file_content:
364 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
365 sane = package_qa_handle_error(6, error_msg, "staging", path, d)
366 bb.utils.unlockfile(lf)
370 # Walk over all files in a directory and call func
371 def package_qa_walk(path, funcs, package,d):
374 #if this will throw an exception, then fix the dict above
375 target_os = bb.data.getVar('TARGET_OS', d, True)
376 target_arch = bb.data.getVar('TARGET_ARCH', d, True)
377 (machine, osabi, abiversion, littleendian, bits32) \
378 = package_qa_get_machine_dict()[target_os][target_arch]
381 for root, dirs, files in os.walk(path):
383 path = os.path.join(root,file)
384 elf = oe.qa.ELFFile(path, bits32)
390 if not func(path, package,d, elf):
395 def package_qa_check_rdepends(pkg, pkgdest, d):
397 if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
398 # Copied from package_ipk.bbclass
399 # boiler plate to update the data
400 localdata = bb.data.createCopy(d)
401 root = "%s/%s" % (pkgdest, pkg)
403 bb.data.setVar('ROOT', '', localdata)
404 bb.data.setVar('ROOT_%s' % pkg, root, localdata)
405 pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
408 bb.data.setVar('PKG', pkgname, localdata)
410 overrides = bb.data.getVar('OVERRIDES', localdata)
412 raise bb.build.FuncFailed('OVERRIDES not defined')
413 overrides = bb.data.expand(overrides, localdata)
414 bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
416 bb.data.update_data(localdata)
418 # Now check the RDEPENDS
419 rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
422 # Now do the sanity check!!!
423 for rdepend in rdepends:
424 if "-dbg" in rdepend:
425 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
426 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
430 # The PACKAGE FUNC to scan each package
431 python do_package_qa () {
432 bb.debug(2, "DO PACKAGE QA")
433 pkgdest = bb.data.getVar('PKGDEST', d, True)
434 packages = bb.data.getVar('PACKAGES',d, True)
436 # no packages should be scanned
440 checks = [package_qa_check_rpath, package_qa_check_dev,
441 package_qa_check_perm, package_qa_check_arch,
442 package_qa_check_desktop, package_qa_hash_style,
443 package_qa_check_dbg]
446 for package in packages.split():
447 if bb.data.getVar('INSANE_SKIP_' + package, d, True):
448 bb.note("package %s skipped" % package)
451 bb.debug(1, "Checking Package: %s" % package)
452 path = "%s/%s" % (pkgdest, package)
453 if not package_qa_walk(path, checks, package, d):
455 if not package_qa_check_rdepends(package, pkgdest, d):
456 rdepends_sane = False
458 if not walk_sane or not rdepends_sane:
459 bb.fatal("QA run found fatal errors. Please consider fixing them.")
460 bb.debug(2, "DONE with PACKAGE QA")
464 # The Staging Func, to check all staging
465 addtask qa_staging after do_populate_sysroot before do_package_stage
466 python do_qa_staging() {
467 bb.debug(2, "QA checking staging")
469 if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
470 bb.fatal("QA staging was broken by the package built above")
473 # Check broken config.log files
474 addtask qa_configure after do_configure before do_compile
475 python do_qa_configure() {
477 bb.debug(1, "Checking sanity of the config.log file")
478 for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
481 statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
482 os.path.join(root,"config.log")
483 if "config.log" in files:
484 if os.system(statement) == 0:
485 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
486 Rerun configure task after fixing this. The path was '%s'""" % root)
488 if "configure.ac" in files:
489 configs.append(os.path.join(root,"configure.ac"))
490 if "configure.in" in files:
491 configs.append(os.path.join(root, "configure.in"))
493 if "gettext" not in bb.data.getVar('P', d, True):
494 if bb.data.inherits_class('native', d) or bb.data.inherits_class('cross', d) or bb.data.inherits_class('crosssdk', d) or bb.data.inherits_class('nativesdk', d):
495 gt = "gettext-native"
496 elif bb.data.inherits_class('cross-canadian', d):
497 gt = "gettext-nativesdk"
500 deps = bb.utils.explode_deps(bb.data.getVar('DEPENDS', d, True) or "")
502 for config in configs:
503 gnu = "grep \"^[[:space:]]*AM_GNU_GETTEXT\" %s >/dev/null" % config
504 if os.system(gnu) == 0:
505 bb.note("""Gettext required but not in DEPENDS for file %s.
506 Missing 'inherit gettext' in recipe?""" % config)