Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).
Expand Down
76 changes: 32 additions & 44 deletions classes/abicheck.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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"
2 changes: 1 addition & 1 deletion conf/layer.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

15 changes: 10 additions & 5 deletions lib/binaryaudit/abicheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""


Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
21 changes: 20 additions & 1 deletion lib/binaryaudit/poky.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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"