fwupd/contrib/ci/oss-fuzz.py
2021-04-08 10:43:29 -05:00

337 lines
12 KiB
Python
Executable File

#!/usr/bin/python3
# pylint: disable=invalid-name,missing-docstring
#
# Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
#
# SPDX-License-Identifier: LGPL-2.1+
#
# pylint: disable=too-many-instance-attributes,no-self-use
import os
import sys
import subprocess
import glob
from typing import Dict, Optional, List, Union
DEFAULT_BUILDDIR = ".ossfuzz"
class Builder:
def __init__(self) -> None:
self.cc = self._ensure_environ("CC", "gcc")
self.cxx = self._ensure_environ("CXX", "g++")
self.builddir = self._ensure_environ("WORK", os.path.realpath(DEFAULT_BUILDDIR))
self.installdir = self._ensure_environ(
"OUT", os.path.realpath(os.path.join(DEFAULT_BUILDDIR, "out"))
)
self.srcdir = self._ensure_environ("SRC", os.path.realpath(".."))
self.ldflags = ["-lpthread", "-lresolv", "-ldl", "-lffi", "-lz"]
# defined in env
self.cflags = ["-Wno-deprecated-declarations"]
if "CFLAGS" in os.environ:
self.cflags += os.environ["CFLAGS"].split(" ")
self.cxxflags = []
if "CXXFLAGS" in os.environ:
self.cxxflags += os.environ["CXXFLAGS"].split(" ")
# set up shared / static
os.environ["PKG_CONFIG"] = "pkg-config --static"
if "PATH" in os.environ:
os.environ["PATH"] = "{}:{}".format(
os.environ["PATH"], os.path.join(self.builddir, "bin")
)
else:
os.environ["PATH"] = os.path.join(self.builddir, "bin")
os.environ["PKG_CONFIG_PATH"] = os.path.join(self.builddir, "lib", "pkgconfig")
# writable
os.makedirs(self.builddir, exist_ok=True)
os.makedirs(self.installdir, exist_ok=True)
def _ensure_environ(self, key: str, value: str) -> str:
""" set the environment unless already set """
if key not in os.environ:
os.environ[key] = value
return os.environ[key]
def checkout_source(self, name: str, url: str, commit: Optional[str] = None) -> str:
""" checkout source tree, optionally to a specific commit """
srcdir_name = os.path.join(self.srcdir, name)
if os.path.exists(srcdir_name):
return srcdir_name
subprocess.run(["git", "clone", url], cwd=self.srcdir, check=True)
if commit:
subprocess.run(["git", "checkout", commit], cwd=srcdir_name, check=True)
return srcdir_name
def build_meson_project(self, srcdir: str, argv) -> None:
""" configure and build the meson project """
srcdir_build = os.path.join(srcdir, DEFAULT_BUILDDIR)
if not os.path.exists(srcdir_build):
subprocess.run(
[
"meson",
"--prefix",
self.builddir,
"--libdir",
"lib",
"--default-library",
"static",
]
+ argv
+ [DEFAULT_BUILDDIR],
cwd=srcdir,
check=True,
)
subprocess.run(["ninja", "install"], cwd=srcdir_build, check=True)
def add_work_includedir(self, value: str) -> None:
""" add a CFLAG """
self.cflags.append("-I{}/{}".format(self.builddir, value))
def add_src_includedir(self, value: str) -> None:
""" add a CFLAG """
self.cflags.append("-I{}/{}".format(self.srcdir, value))
def add_build_ldflag(self, value: str) -> None:
""" add a LDFLAG """
self.ldflags.append(os.path.join(self.builddir, value))
def substitute(self, src: str, replacements: Dict[str, str]) -> str:
""" map changes """
dst = os.path.basename(src).replace(".in", "")
with open(os.path.join(self.srcdir, src), "r") as f:
blob = f.read()
for key in replacements:
blob = blob.replace(key, replacements[key])
with open(os.path.join(self.builddir, dst), "w") as out:
out.write(blob)
return dst
def compile(self, src: str) -> str:
""" compile a specific source file """
argv = [self.cc]
argv.extend(self.cflags)
fullsrc = os.path.join(self.srcdir, src)
if not os.path.exists(fullsrc):
fullsrc = os.path.join(self.builddir, src)
dst = os.path.basename(src).replace(".c", ".o")
argv.extend(["-c", fullsrc, "-o", os.path.join(self.builddir, dst)])
print("building {} into {}".format(src, dst))
try:
subprocess.run(argv, cwd=self.srcdir, check=True)
except subprocess.CalledProcessError as e:
print(e)
sys.exit(1)
return os.path.join(self.builddir, "{}".format(dst))
def link(self, objs: List[str], dst: str) -> None:
""" link multiple obects into a binary """
argv = [self.cxx] + self.cxxflags
for obj in objs:
if obj.startswith("-"):
argv.append(obj)
else:
argv.append(os.path.join(self.builddir, obj))
argv += ["-o", os.path.join(self.installdir, dst)]
argv += self.ldflags
print("building {} into {}".format(",".join(objs), dst))
subprocess.run(argv, cwd=self.srcdir, check=True)
def write_header(
self, dst: str, defines: Dict[str, Optional[Union[str, int]]]
) -> None:
""" write a header file """
dstdir = os.path.join(self.builddir, os.path.dirname(dst))
os.makedirs(dstdir, exist_ok=True)
print("writing {}".format(dst))
with open(os.path.join(dstdir, os.path.basename(dst)), "w") as f:
for key in defines:
value = defines[key]
if value is not None:
if isinstance(value, int):
f.write("#define {} {}\n".format(key, value))
else:
f.write('#define {} "{}"\n'.format(key, value))
else:
f.write("#define {}\n".format(key))
self.add_work_includedir(os.path.dirname(dst))
def makezip(self, dst: str, globstr: str) -> None:
""" create a zip file archive from a glob """
argv = ["zip", "--junk-paths", os.path.join(self.installdir, dst)] + glob.glob(
os.path.join(self.srcdir, globstr)
)
print("assembling {}".format(dst))
subprocess.run(argv, cwd=self.srcdir, check=True)
def grep_meson(self, src: str, token: str = "fuzzing") -> List[str]:
""" find source files tagged with a specific comment """
srcs = []
with open(os.path.join(self.srcdir, src, "meson.build"), "r") as f:
for line in f.read().split("\n"):
if line.find(token) == -1:
continue
srcs.append(
os.path.join(
src,
line.strip()
.replace("'", "")
.replace(",", "")
.replace(" ", "")
.split("#")[0],
)
)
return srcs
class Fuzzer:
def __init__(self, name, srcdir=None, globstr=None, pattern=None) -> None:
self.name = name
self.srcdir = srcdir or name
self.globstr = globstr or "{}*".format(name)
self.pattern = pattern or "{}-firmware".format(name)
@property
def new_gtype(self) -> str:
return "fu_{}_new".format(self.pattern).replace("-", "_")
@property
def header(self) -> str:
return "fu-{}.h".format(self.pattern)
def _build(bld: Builder) -> None:
# GLib
src = bld.checkout_source("glib", url="https://gitlab.gnome.org/GNOME/glib.git")
bld.build_meson_project(
src,
[
"-Dlibmount=disabled",
"-Dselinux=disabled",
"-Dnls=disabled",
"-Dlibelf=disabled",
"-Dbsymbolic_functions=false",
"-Dtests=false",
"-Dinternal_pcre=true",
],
)
bld.add_work_includedir("include/glib-2.0")
bld.add_work_includedir("lib/glib-2.0/include")
bld.add_build_ldflag("lib/libgio-2.0.a")
bld.add_build_ldflag("lib/libgmodule-2.0.a")
bld.add_build_ldflag("lib/libgobject-2.0.a")
bld.add_build_ldflag("lib/libglib-2.0.a")
bld.add_build_ldflag("lib/libgthread-2.0.a")
# JSON-GLib
src = bld.checkout_source(
"json-glib", url="https://gitlab.gnome.org/GNOME/json-glib.git"
)
bld.build_meson_project(
src, ["-Dgtk_doc=disabled", "-Dtests=false", "-Dintrospection=disabled"]
)
bld.add_work_includedir("include/json-glib-1.0/json-glib")
bld.add_work_includedir("include/json-glib-1.0")
bld.add_build_ldflag("lib/libjson-glib-1.0.a")
# libxmlb
src = bld.checkout_source("libxmlb", url="https://github.com/hughsie/libxmlb.git")
bld.build_meson_project(
src, ["-Dgtkdoc=false", "-Dintrospection=false", "-Dtests=false"]
)
bld.add_work_includedir("include/libxmlb-2")
bld.add_work_includedir("include/libxmlb-2/libxmlb")
bld.add_build_ldflag("lib/libxmlb.a")
# write required headers
bld.write_header("libfwupd/fwupd-version.h", {})
bld.write_header(
"config.h",
{
"FWUPD_DATADIR": "/tmp",
"FWUPD_LOCALSTATEDIR": "/tmp",
"FWUPD_PLUGINDIR": "/tmp",
"FWUPD_SYSCONFDIR": "/tmp",
"HAVE_REALPATH": None,
"PACKAGE_NAME": "fwupd",
"PACKAGE_VERSION": "0.0.0",
},
)
# libfwupd + libfwupdplugin
built_objs: List[str] = []
bld.add_src_includedir("fwupd")
for path in ["fwupd/libfwupd", "fwupd/libfwupdplugin"]:
bld.add_src_includedir(path)
for src in bld.grep_meson(path):
built_objs.append(bld.compile(src))
# dummy binary entrypoint
if "LIB_FUZZING_ENGINE" in os.environ:
built_objs.append(os.environ["LIB_FUZZING_ENGINE"])
else:
built_objs.append(bld.compile("fwupd/libfwupdplugin/fu-fuzzer-main.c"))
# built in formats
for fzr in [Fuzzer("dfuse"), Fuzzer("fmap"), Fuzzer("ihex"), Fuzzer("srec")]:
src = bld.substitute(
"fwupd/libfwupdplugin/fu-fuzzer-firmware.c.in",
{
"@FIRMWARENEW@": fzr.new_gtype,
"@INCLUDE@": os.path.join("libfwupdplugin", fzr.header),
},
)
bld.link([bld.compile(src)] + built_objs, "{}_fuzzer".format(fzr.name))
bld.makezip(
"{}_fuzzer_seed_corpus.zip".format(fzr.name),
"fwupd/src/fuzzing/firmware/{}".format(fzr.globstr),
)
# plugins
for fzr in [
Fuzzer("bcm57xx"),
Fuzzer("ccgx-dmc", srcdir="ccgx", globstr="ccgx-dmc*.bin"),
Fuzzer("ccgx", globstr="ccgx*.cyacd"),
Fuzzer("cros-ec"),
Fuzzer("ebitdo"),
Fuzzer("efi-filesystem", srcdir="intel-spi", pattern="efi-firmware-filesystem"),
Fuzzer("efi-volume", srcdir="intel-spi", pattern="efi-firmware-volume"),
Fuzzer("elantp"),
Fuzzer("hailuck-kbd", srcdir="hailuck", globstr="ihex*"),
Fuzzer("ifd", srcdir="intel-spi"),
Fuzzer("pixart", srcdir="pixart-rf", pattern="pxi-firmware"),
Fuzzer("solokey"),
Fuzzer("synaprom", srcdir="synaptics-prometheus"),
Fuzzer("synaptics-mst"),
Fuzzer("synaptics-rmi"),
Fuzzer("wacom-usb", pattern="wac-firmware", globstr="wacom*"),
]:
fuzz_objs = []
for obj in bld.grep_meson("fwupd/plugins/{}".format(fzr.srcdir)):
fuzz_objs.append(bld.compile(obj))
src = bld.substitute(
"fwupd/libfwupdplugin/fu-fuzzer-firmware.c.in",
{
"@FIRMWARENEW@": fzr.new_gtype,
"@INCLUDE@": os.path.join("plugins", fzr.srcdir, fzr.header),
},
)
fuzz_objs.append(bld.compile(src))
bld.link(fuzz_objs + built_objs, "{}_fuzzer".format(fzr.name))
bld.makezip(
"{}_fuzzer_seed_corpus.zip".format(fzr.name),
"fwupd/src/fuzzing/firmware/{}".format(fzr.globstr),
)
if __name__ == "__main__":
_builder = Builder()
_build(_builder)
sys.exit(0)