fwupd/contrib/ci/oss-fuzz.py
Simon Ho 5e67108ab8
plugins: add new plugin for Synaptics CAPE devices (#3746)
CAPE family is Audio DSP for a board range of applications in IOT, PC
and mobile can be interfaced via I2C, UART or USB interface. This patch
is only for CX31993 and CX31988 chips, there is not immediate plans is
to add support to other CAPE devices.

CX31993 have two separate firmware .hid file for for each partition. It
need to convert two .hid files into a .fw file for fwupd tool to
consume.

Currently, this patch is only support for EPOS headsets with basic
firmware update feature. Either new code singing or manifest.xml are
unsupported yet.

The code has been tested with CX31993 EVK board.

A test firmware file is put at 'src/fuzzing/firmware/synaptics-cape.fw'

synaptics-cape: Port to new FuProgress API and style fixups

synaptics-cape: Fix compile errors and add missing test fw file

Signed-off-by: Simon Ho <simon.ho@synaptics.com>

synaptics-cape: Fix fuzzer test

Signed-off-by: Simon Ho <simon.ho@synaptics.com>

synaptics-cape: Fix progress bar number

Signed-off-by: Simon Ho <simon.ho@synaptics.com>

synaptics-cape: Mark the fuzzing target

trivial: Use a stable GLib branch for fuzzing

synaptics-cape: Fix progress bar number

Signed-off-by: Simon Ho <simon.ho@synaptics.com>

synaptics-cape: Fix readme

synaptics-cape: Style fixups

synaptics-cape: Fix progress bar percentage

synaptics-cape: Style fixups
2021-09-15 03:42:07 +08:00

365 lines
13 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", "-llzma"]
# 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
# get rid of token
line = line.split("#")[0]
# get rid of variable
try:
line = line.split("=")[1]
except IndexError:
pass
# get rid of whitespace
for char in ["'", ",", " "]:
line = line.replace(char, "")
# all done
srcs.append(os.path.join(src, line))
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", commit="glib-2-68"
)
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"),
Fuzzer("efi-filesystem", pattern="efi-firmware-filesystem"),
Fuzzer("efi-volume", pattern="efi-firmware-volume"),
Fuzzer("ifd"),
]:
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("acpi-phat", pattern="acpi-phat"),
Fuzzer("bcm57xx"),
Fuzzer("ccgx-dmc", srcdir="ccgx", globstr="ccgx-dmc*.bin"),
Fuzzer("ccgx", globstr="ccgx*.cyacd"),
Fuzzer("cros-ec"),
Fuzzer("ebitdo"),
Fuzzer("elantp"),
Fuzzer("hailuck-kbd", srcdir="hailuck", globstr="ihex*"),
Fuzzer("pixart", srcdir="pixart-rf", pattern="pxi-firmware"),
Fuzzer("redfish-smbios", srcdir="redfish", pattern="redfish-smbios"),
Fuzzer("solokey"),
Fuzzer("synaprom", srcdir="synaptics-prometheus"),
Fuzzer("synaptics-cape"),
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__":
# install missing deps here rather than patching the Dockerfile in oss-fuzz
try:
subprocess.check_call(
["apt-get", "install", "-y", "liblzma-dev"], stdout=open(os.devnull, "wb")
)
except FileNotFoundError:
pass
except subprocess.CalledProcessError as e:
print(e.output)
sys.exit(1)
_builder = Builder()
_build(_builder)
sys.exit(0)