diff --git a/README.md b/README.md index ac3e7a0..4a7e303 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Yocto layer for ELF binary compliance validation. # Dependencies * URI: http://git.yoctoproject.org/clean/cgit.cgi/poky -* Branch: dunfell|gatesgarth|hardknott|honister +* Branch: kirkstone|scarthgap|whinlatter # ABI compliance @@ -30,12 +30,13 @@ After your first build to collect baseline data, set the variable below to the b BINARY_AUDIT_REFERENCE_BASEDIR = "/path/to/buildhistory.baseline" ``` -The ABI comparison is done during [the Package QA mechanism](https://docs.yoctoproject.org/3.2/ref-manual/ref-qa-checks.html), allowing you to control whether if an ABI change is an error or a warning. Then, to enable alerting for ABI changes, add the `abi-changed` QA test using _one of_ the lines here: +The ABI comparison is done during [the Package QA mechanism](https://docs.yoctoproject.org/3.2/ref-manual/ref-qa-checks.html). ABI changes are reported as **warnings by default**, no configuration is required to enable this. + +To promote ABI changes to a build error instead, add the following to your `local.conf`: ```bitbake -WARN_QA_append = " abi-changed" -# --- or --- -ERROR_QA_append = " abi-changed" +ERROR_QA:append = " abi-changed" +WARN_QA:remove = " abi-changed" ``` The tools used to perform the compatibility verification is [abicompat](https://sourceware.org/libabigail/manual/abicompat.html). diff --git a/classes/abicheck.bbclass b/classes/abicheck.bbclass index a1ba520..c7fa176 100644 --- a/classes/abicheck.bbclass +++ b/classes/abicheck.bbclass @@ -4,9 +4,9 @@ inherit insane BUILDHISTORY_FEATURES += "abicheck" -DEPENDS_append_class-target = " libabigail-native" +DEPENDS:append:class-target = "${@ ' libabigail-native' if d.getVar('ABI_CHECK_SKIP') != '1' else ''}" -IMG_DIR="${WORKDIR}/image" +IMG_DIR = "${WORKDIR}/image" python binary_audit_gather_abixml() { import glob, os, time @@ -51,79 +51,70 @@ python binary_audit_gather_abixml() { } # Target binaries are the only interest. -do_install[postfuncs] += "${@ "binary_audit_gather_abixml" if ("class-target" == d.getVar("CLASSOVERRIDE")) else "" }" +do_install[postfuncs] += "${@ 'binary_audit_gather_abixml' if (d.getVar('CLASSOVERRIDE') == 'class-target' and d.getVar('ABI_CHECK_SKIP') != '1') else ''}" do_install[vardepsexclude] += "${@ "binary_audit_gather_abixml" if ("class-target" == d.getVar("CLASSOVERRIDE")) else "" }" -QARECIPETEST[abi-changed] = "package_qa_binary_audit_abixml_compare_to_ref" def package_qa_binary_audit_abixml_compare_to_ref(pn, d, messages): import glob, os, time - from binaryaudit import util + import oe.qa from binaryaudit import abicheck t0 = time.monotonic() - recipe_suppr = d.getVar("WORKDIR") + "/abi*.suppr" - suppr = glob.glob(recipe_suppr) if os.path.isfile(str(d.getVar("BINARY_AUDIT_GLOBAL_SUPPRESSION_FILE"))): suppr += [d.getVar("BINARY_AUDIT_GLOBAL_SUPPRESSION_FILE")] else: - util.note("No global suppression found") - - util.note("SUPPRESSION FILES: {}".format(str(suppr))) - + bb.debug(1, "No global suppression found") + bb.debug(1, "SUPPRESSION FILES: {}".format(str(suppr))) dest_basedir = binary_audit_get_create_pkg_dest_basedir(d) cur_abixml_dir = os.path.join(dest_basedir, "abixml") if not os.path.isdir(cur_abixml_dir): - util.note("No ABI dump found in the current build for '{}' under '{}'".format(pn, cur_abixml_dir)) + bb.debug(1, "No ABI dump found in the current build for '{}' under '{}'".format(pn, cur_abixml_dir)) return ref_basedir = d.getVar("BINARY_AUDIT_REFERENCE_BASEDIR") - if len(ref_basedir) < 1: - util.note("BINARY_AUDIT_REFERENCE_BASEDIR not set, no reference ABI comparison to perform") + if not ref_basedir or len(ref_basedir) < 1: + bb.debug(1, "BINARY_AUDIT_REFERENCE_BASEDIR not set, no reference ABI comparison to perform") return if not os.path.isdir(ref_basedir): - util.note("No binary audit reference ABI found under '{}'".format(ref_basedir)) + bb.debug(1, "No binary audit reference ABI found under '{}'".format(ref_basedir)) return - util.note("BINARY_AUDIT_REFERENCE_BASEDIR = \"{}\"".format(ref_basedir)) + bb.note("BINARY_AUDIT_REFERENCE_BASEDIR = \"{}\"".format(ref_basedir)) cur_abidiff_dir = os.path.join(dest_basedir, "abidiff") if not os.path.exists(cur_abidiff_dir): bb.utils.mkdirhier(cur_abidiff_dir) - - for fpath in glob.iglob("{}/packages/*/**/{}/binaryaudit".format(ref_basedir, pn), recursive = True): + ref_found = False + for fpath in glob.iglob("{}/packages/*/**/{}/binaryaudit".format(ref_basedir, pn), recursive=True): + ref_found = True ref_abixml_dir = os.path.join(fpath, "abixml") if not os.path.isdir(ref_abixml_dir): - util.note("No ABI reference found for '{}' under '{}'".format(pn, ref_abixml_dir)) + bb.debug(1, "No ABI reference found for '{}' under '{}'".format(pn, ref_abixml_dir)) continue - # A correct reference history dir for this package is found, proceed - # to see if there's something to compare + bb.note("Found reference ABI for '{}' at '{}'".format(pn, fpath)) for xml_fn in os.listdir(cur_abixml_dir): if not xml_fn.endswith('xml'): continue ref_xml_fpath = os.path.join(ref_abixml_dir, xml_fn) if not os.path.isfile(ref_xml_fpath): - util.note("File '{}' is not present in the reference ABI dump".format(xml_fn)) + bb.debug(1, "File '{}' is not present in the reference ABI dump".format(xml_fn)) continue - cur_xml_fpath = os.path.join(cur_abixml_dir, xml_fn); + cur_xml_fpath = os.path.join(cur_abixml_dir, xml_fn) with open(cur_xml_fpath) as f: xml = f.read() - f.close() - # Care only about DSO for now sn = abicheck.get_soname_from_xml(xml) - # XXX Handle error cases, eg xml file was garbage, etc. if len(sn) > 0: - # XXX Implement suppression handling ret, out, cmd = abicheck.compare(ref_xml_fpath, cur_xml_fpath, suppr) - util.note(" ".join(cmd)) + bb.note("abidiff command: " + " ".join(cmd)) status_bits = abicheck.diff_get_bits(ret) @@ -134,30 +125,27 @@ def package_qa_binary_audit_abixml_compare_to_ref(pn, d, messages): f.write(status_bits[k] + "\n") k = k + 1 f.write(status_bits[k]) - f.close() cur_out_fpath = os.path.join(cur_abidiff_dir, ".".join([os.path.splitext(xml_fn)[0], "out"])) with open(cur_out_fpath, "w") as f: f.write(out) - f.close() - - if abicheck.diff_is_ok(ret): - continue + bb.note("Generated abidiff for {} in {}".format(xml_fn, cur_abidiff_dir)) - #for n in range(8): - # bb.note("bit '{}': '{}'".format(n, (ret >> n) & 1)) - - status_ln = " ".join(status_bits) - # XXX Just warn for now if there's anythnig non 0 in the status. - # Should be made finer configurable through local.conf. - util.add_message(messages, 'abi-changed', - '%s: ABI changed from reference build, logs: %s' - % (pn, out)) + if not abicheck.diff_is_ok(ret): + oe.qa.handle_error("abi-changed", + "%s: ABI changed from reference build, logs: %s" % (pn, out), d) + if not ref_found: + bb.note("No reference ABI found for '{}' in '{}' - package may be new in this build".format(pn, ref_basedir)) t1 = time.monotonic() duration_fl = cur_abidiff_dir + ".duration" - bb.note("binary_audit_abixml_compare_to_ref: start={}, end={}, duration={}".format(t0, t1, t1 - t0)) + bb.note("binary_audit_compare_abixml_to_ref: start={}, end={}, duration={}".format(t0, t1, t1 - t0)) with open(duration_fl, "w") as f: f.write(u"{}".format(t1 - t0)) - f.close() +python __anonymous() { + bb.utils._context["package_qa_binary_audit_abixml_compare_to_ref"] = package_qa_binary_audit_abixml_compare_to_ref +} + +QARECIPETEST[abi-changed] = "package_qa_binary_audit_abixml_compare_to_ref" +WARN_QA:append = " abi-changed" diff --git a/conf/layer.conf b/conf/layer.conf index 8c48f24..41ccef6 100644 --- a/conf/layer.conf +++ b/conf/layer.conf @@ -9,7 +9,7 @@ BBFILE_PATTERN_binary-audit-layer := "^${LAYERDIR}/" BBFILE_PRIORITY_binary-audit-layer = "10" LAYERDEPENDS_binary-audit-layer = "core" -LAYERSERIES_COMPAT_binary-audit-layer = "thud warrior zeus dunfell gatesgarth hardknott honister kirkstone" +LAYERSERIES_COMPAT_binary-audit-layer = "thud warrior zeus dunfell gatesgarth hardknott honister kirkstone scarthgap whinlatter" BINARY_AUDIT_LAYERDIR = "${LAYERDIR}" diff --git a/lib/binaryaudit/abicheck.py b/lib/binaryaudit/abicheck.py index dbacbc1..541421b 100644 --- a/lib/binaryaudit/abicheck.py +++ b/lib/binaryaudit/abicheck.py @@ -15,10 +15,12 @@ def is_elf(fn): def get_soname_from_xml(xml): - r = ElementTree.fromstring(xml) + if not xml or not xml.startswith('<'): + return "" try: + r = ElementTree.fromstring(xml) return r.attrib["soname"] - except (AttributeError, KeyError): + except (ElementTree.ParseError, AttributeError, KeyError): return "" @@ -36,8 +38,10 @@ def _serialize(cmd): return process.returncode, out -def serialize(fn): +def serialize(fn, debug_info_dir=None): cmd = ["abidw", "--no-corpus-path", fn] + if debug_info_dir and os.path.isdir(debug_info_dir): + cmd += ["--debug-info-dir", debug_info_dir] status, out = _serialize(cmd) return status, out, cmd @@ -86,11 +90,12 @@ def compare(ref, cur, suppr=[]): return process.returncode, out, cmd -def serialize_artifacts(adir, id): +def serialize_artifacts(adir, id, debug_info_dir=None): ''' Recursively serialize binary artifacts starting at the given image directory(id), yields serialized output and filename Parameters: adir (str): path to abixml directory id (str): image directory- result of calling d.getVar("IMG_DIR") + debug_info_dir (str): optional path to directory containing debug info (.debug files) ''' for fn in glob.iglob(id + "/**/**", recursive=True): if os.path.isfile(fn) and not os.path.islink(fn): @@ -103,7 +108,7 @@ def serialize_artifacts(adir, id): continue # If there's no error, out is the XML representation - ret, out, cmd = serialize(fn) + ret, out, cmd = serialize(fn, debug_info_dir) util.note(" ".join(cmd)) if not 0 == ret: util.error(out) diff --git a/lib/binaryaudit/poky.py b/lib/binaryaudit/poky.py index 01c1d7e..e9bcd67 100644 --- a/lib/binaryaudit/poky.py +++ b/lib/binaryaudit/poky.py @@ -37,7 +37,26 @@ def retrieve_baseline(db_conn, prod_id): os.makedirs(extractdir) with tarfile.open(data_fl.name, "r:gz") as tgz: - tgz.extractall(extractdir) + def is_within_directory(directory, target): + + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + + return prefix == abs_directory + + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): + + for member in tar.getmembers(): + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted Path Traversal in Tar File") + + tar.extractall(path, members, numeric_owner=numeric_owner) + + + safe_extract(tgz, extractdir) tgz.close() os.unlink(data_fl.name) # Depends on how we pack, but the first sibling named "buildhistory" should be it. diff --git a/recipes-devtools/liabigail/libabigail_1.8.2.bb b/recipes-devtools/liabigail/libabigail_2.8.bb similarity index 56% rename from recipes-devtools/liabigail/libabigail_1.8.2.bb rename to recipes-devtools/liabigail/libabigail_2.8.bb index e49b18c..8f20774 100644 --- a/recipes-devtools/liabigail/libabigail_1.8.2.bb +++ b/recipes-devtools/liabigail/libabigail_2.8.bb @@ -1,39 +1,42 @@ -SUMMARY = "The ABI Generic Analysis and Instrumentation Library " +SUMMARY = "The ABI Generic Analysis and Instrumentation Library" HOMEPAGE = "https://sourceware.org/libabigail" -LICENSE = "LGPLv3" +LICENSE = "LGPL-3.0-or-later" SECTION = "devel" -SHA512SUM="fa8edaf39632e26430481f15e962a098459eac087074e85ca055293ba324ec5944c45880fcb36f1c54a64652605a439cbf9247dfea9bfd3ec502cc7292dd1c8d" -SRC_URI[md5sum] = "bd8509b286ff39fe82107a3847ee9f39" -SRC_URI[sha256sum] = "86347c9f0a8666f263fd63f8c3fe4c4f9cb1bdb3ec4260ecbaf117d137e89787" +SRC_URI = "https://mirrors.kernel.org/sourceware/libabigail/libabigail-${PV}.tar.xz" +SRC_URI[sha256sum] = "0f52b1ab7997ee2f7895afb427f24126281f66a4756ba2c62bce1a17b546e153" -SRC_URI = "https://mirrors.kernel.org/sourceware/libabigail/libabigail-${PV}.tar.gz;sha512sum=${SHA512SUM}" +LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=0bcd48c3bdfef0c9d9fd17726e4b7dab" -LIC_FILES_CHKSUM = " \ - file://COPYING;md5=2b3c1a10dd8e84f2db03cb00825bcf95 \ -" +DEPENDS += "elfutils libxml2 xxhash" -DEPENDS += "elfutils libxml2" - -S = "${WORKDIR}/libabigail-${PV}" +python __anonymous() { + if d.getVar("UNPACKDIR"): + d.setVar("S", "${UNPACKDIR}/libabigail-${PV}") + else: + d.setVar("S", "${WORKDIR}/libabigail-${PV}") +} inherit autotools pkgconfig PACKAGECONFIG ??= "${@bb.utils.contains('PACKAGE_CLASSES', 'package_rpm', 'rpm', '', d)} \ ${@bb.utils.contains('PACKAGE_CLASSES', 'package_deb', 'deb', '', d)} \ tar python3" + PACKAGECONFIG[rpm] = "--enable-rpm,--disable-rpm,rpm" PACKAGECONFIG[deb] = "--enable-deb,--disable-deb,deb" PACKAGECONFIG[tar] = "--enable-tar,--disable-tar,tar" -PACKAGECONFIG[zip-archive] = "--enable-zip-archive,--disable-zip-archive,zip-archive" PACKAGECONFIG[apidoc] = "--enable-apidoc,--disable-apidoc,apidoc" PACKAGECONFIG[manual] = "--enable-manual,--disable-manual,manual" PACKAGECONFIG[bash-completion] = "--enable-bash-completion,--disable-bash-completion,bash-completion" PACKAGECONFIG[fedabipkgdiff] = "--enable-fedabipkgdiff,--disable-fedabipkgdiff,fedabipkgdiff" PACKAGECONFIG[python3] = "--enable-python3,--disable-python3,python3" -RDEPENDS_${PN} += "${@bb.utils.contains('PACKAGECONFIG', 'python3', 'python3', '', d)}" -RDEPENDS_${PN} += "${@bb.utils.contains('PACKAGECONFIG', 'deb', 'dpkg', '', d)}" +RDEPENDS:${PN} += "${@bb.utils.contains('PACKAGECONFIG', 'python3', 'python3', '', d)}" +RDEPENDS:${PN} += "${@bb.utils.contains('PACKAGECONFIG', 'deb', 'dpkg', '', d)}" -BBCLASSEXTEND = "native nativesdk" +PACKAGECONFIG:remove:class-native = "rpm deb" +PACKAGECONFIG:remove:class-nativesdk = "rpm deb" + +BBCLASSEXTEND = "native nativesdk"