Use honggfuzz to fuzz firmware rather than AFL

This has better multi-core performance and can run in persistent mode -- which
allows us to construct a test harness of all the parsers (which takes time) and
then just reuse the process for lots of different data.
This commit is contained in:
Richard Hughes 2020-12-08 16:02:03 +00:00
parent 531b8b417f
commit c4ca8e25d1
35 changed files with 352 additions and 195 deletions

View File

@ -133,9 +133,9 @@ Fuzzing
There are several automated fuzzing tests in fwupd. These take some time to run: There are several automated fuzzing tests in fwupd. These take some time to run:
CC=afl-gcc meson --default-library=static ../ CC=hfuzz-clang meson --default-library=static -Dtmpdir=/tmp -Dsystemd_root_prefix=/tmp ../
AFL_HARDEN=1 ninja ninja install
ninja fuzz-synaptics-rmi
ninja fuzz-firmware ninja fuzz-firmware
ninja fuzz-smbios ninja fuzz-smbios
ninja fuzz-efidbx ninja fuzz-efidbx
ninja fuzz-tpm-eventlog

View File

@ -1,78 +0,0 @@
#!/usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1+
import argparse
import sys
import subprocess
import os
def main():
parser = argparse.ArgumentParser(description='Run afl-fuzz on all cores')
parser.add_argument('--input', '-i', help='fuzzing input directory')
parser.add_argument('--output', '-o', help='findings output directory')
parser.add_argument('--command', type=str, help='fuzzer tool command')
parser.add_argument('path', type=str, help='the fuzzer tool')
args = parser.parse_args()
if not args.input and not args.output:
print('-i and -o required')
return 1
if not args.path:
print('tool name required')
return 1
# create if not already exists
if not os.path.exists(args.output):
os.makedirs(args.output)
# run the main instance
envp = None
argv = [
'afl-fuzz',
'-m300',
'-i',
args.input,
'-o',
args.output,
'-M',
'fuzzer00',
args.path,
]
if args.command:
argv.append(args.command)
argv.append('@@')
print(argv)
p = subprocess.Popen(argv, env=envp)
# run the secondary instances
cs = []
for i in range(1, os.cpu_count()):
argv = [
'afl-fuzz',
'-m300',
'-i',
args.input,
'-o',
args.output,
'-S',
'fuzzer%02i' % i,
args.path,
]
if args.command:
argv.append(args.command)
argv.append('@@')
print(argv)
cs.append(subprocess.Popen(argv, env=envp, stdout=subprocess.DEVNULL))
# wait for the main instance
try:
p.wait()
except KeyboardInterrupt as _:
pass
for c in cs:
c.terminate()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -73,7 +73,6 @@ warning_flags = [
'-Wformat-signedness', '-Wformat-signedness',
'-Wignored-qualifiers', '-Wignored-qualifiers',
'-Wimplicit-function-declaration', '-Wimplicit-function-declaration',
'-Wincompatible-pointer-types-discards-qualifiers',
'-Winit-self', '-Winit-self',
'-Wlogical-op', '-Wlogical-op',
'-Wmaybe-uninitialized', '-Wmaybe-uninitialized',
@ -113,6 +112,12 @@ warning_flags = [
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
add_project_arguments(cc.get_supported_arguments(warning_flags), language : 'c') add_project_arguments(cc.get_supported_arguments(warning_flags), language : 'c')
# use honggfuzz for parallel persistent fuzzing
honggfuzz = find_program('honggfuzz', required: cc.has_function('HF_ITER'))
if honggfuzz.found()
conf.set('HAVE_HF_ITER', '1')
endif
if not meson.is_cross_build() if not meson.is_cross_build()
add_project_arguments('-fstack-protector-strong', language : 'c') add_project_arguments('-fstack-protector-strong', language : 'c')
endif endif

View File

@ -1,6 +0,0 @@
Fuzzing
=======
CC=afl-gcc meson --default-library=static ../
AFL_HARDEN=1 ninja
afl-fuzz -m 300 -i fuzzing -o findings ./plugins/dfu/dfu-tool --force dump @@

View File

@ -1,9 +0,0 @@
run_target('fuzz-optionrom',
command: [
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'),
'--command', 'rom',
'-i', meson.current_source_dir(),
'-o', join_paths(meson.current_build_dir(), '..', 'findings'),
optionrom_tool,
],
)

View File

@ -79,5 +79,17 @@ if get_option('tests')
install_dir : installed_test_bindir, install_dir : installed_test_bindir,
) )
test('optionrom-self-test', e, env : testdatadirs) # added to installed-tests test('optionrom-self-test', e, env : testdatadirs) # added to installed-tests
subdir('fuzzing') endif
if honggfuzz.found()
run_target('fuzz-optionrom',
command: [
honggfuzz,
'--input', join_paths(meson.current_source_dir(), 'fuzzing'),
'--output', join_paths(meson.current_build_dir(), 'fuzzing-corpus'),
'--workspace', join_paths(meson.current_build_dir(), 'fuzzing-findings'),
'--verifier',
'--', optionrom_tool, 'rom', '___FILE___',
],
)
endif endif

View File

@ -1,20 +0,0 @@
synaptics_example0x = custom_target('example0x.img',
output: 'example0x.img',
command: [synaptics_rmi_dump, 'gen0x', '@OUTPUT@'],
)
synaptics_example10 = custom_target('example10.img',
output: 'example10.img',
command: [synaptics_rmi_dump, 'gen10', '@OUTPUT@'],
)
run_target('fuzz-synaptics-rmi',
command: [
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'),
'-i', meson.current_build_dir(),
'-o', join_paths(meson.current_build_dir(), '..', 'findings'),
synaptics_rmi_dump,
],
depends: [
synaptics_example0x,
synaptics_example10,
],
)

View File

@ -56,5 +56,4 @@ if get_option('tests')
], ],
c_args : cargs c_args : cargs
) )
subdir('fuzzing')
endif endif

View File

@ -0,0 +1 @@
Spec ID Event03

View File

@ -98,11 +98,15 @@ if get_option('man')
) )
endif endif
run_target('fuzz-tpm-eventlog', if honggfuzz.found()
command: [ run_target('fuzz-tpm-eventlog',
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'), command: [
'-i', join_paths(meson.current_source_dir(), 'tests'), honggfuzz,
'-o', join_paths(meson.current_build_dir(), 'findings'), '--input', join_paths(meson.current_source_dir(), 'fuzzing'),
fwupdtpmevlog, '--output', join_paths(meson.current_build_dir(), 'fuzzing-corpus'),
], '--workspace', join_paths(meson.current_build_dir(), 'fuzzing-findings'),
) '--verifier',
'--', fwupdtpmevlog, '___FILE___',
],
)
endif

View File

@ -137,12 +137,15 @@ if get_option('man')
) )
endif endif
run_target('fuzz-efidbx', if honggfuzz.found()
command: [ run_target('fuzz-efidbx',
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'), command: [
'-i', join_paths(meson.current_source_dir(), 'fuzzing'), honggfuzz,
'-o', join_paths(meson.current_build_dir(), 'findings'), '--input', join_paths(meson.current_source_dir(), 'fuzzing'),
'--command', uefi_dbx_fuzzer, '--output', join_paths(meson.current_build_dir(), 'fuzzing-corpus'),
fwupdtool, '--workspace', join_paths(meson.current_build_dir(), 'fuzzing-findings'),
], '--verifier',
) '--', uefi_dbx_fuzzer, '___FILE___',
],
)
endif

View File

@ -1,58 +1,236 @@
/* /*
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com> * Copyright (C) 2019-2020 Richard Hughes <richard@hughsie.com>
* *
* SPDX-License-Identifier: LGPL-2.1+ * SPDX-License-Identifier: LGPL-2.1+
*/ */
#include "config.h" #include "config.h"
#include "fu-common.h" #include <glib/gi18n.h>
#include "fu-srec-firmware.h" #include <locale.h>
#include "fu-ihex-firmware.h" #include <stdlib.h>
#include "fu-engine.h"
typedef struct {
gboolean verbose;
gint timeout; /* ms */
GPtrArray *array; /* element-type FuFirmware */
FuEngine *engine;
} FuUtil;
extern void HF_ITER(guint8 **buf, gsize *len);
static gboolean
fu_firmware_dump_parse (FuUtil *self,
FuFirmware *firmware,
GBytes *fw,
GError **error)
{
gboolean ret;
gdouble elapsed_ms;
g_autoptr(GError) error_local = NULL;
g_autoptr(GTimer) timer = g_timer_new ();
/* parse, relaxing all the restrictions */
ret = fu_firmware_parse (firmware, fw,
FWUPD_INSTALL_FLAG_NO_SEARCH |
FWUPD_INSTALL_FLAG_IGNORE_VID_PID |
FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM,
&error_local);
/* a timeout is more important than the actual parse failure */
elapsed_ms = g_timer_elapsed (timer, NULL) * 1000;
if (self->timeout > 0 &&
elapsed_ms > self->timeout) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT,
"%s took %.1fms (more than limit of %ims)",
G_OBJECT_TYPE_NAME (firmware),
elapsed_ms,
self->timeout);
return FALSE;
}
/* success? */
if (!ret) {
g_propagate_prefixed_error (error,
g_steal_pointer (&error_local),
"%s failed in %.0lfms: ",
G_OBJECT_TYPE_NAME (firmware),
elapsed_ms);
return FALSE;
}
return TRUE;
}
static gboolean
fu_firmware_dump_iter (FuUtil *self, GBytes *blob, GError **error)
{
gboolean any_okay = FALSE;
for (guint i = 0; i < self->array->len; i++) {
FuFirmware *firmware = g_ptr_array_index (self->array, i);
g_autoptr(GError) error_local = NULL;
g_autofree gchar *str = NULL;
if (!fu_firmware_dump_parse (self, firmware, blob, &error_local)) {
/* timeout so bail */
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT)) {
g_propagate_error (error, g_steal_pointer (&error_local));
return FALSE;
}
g_printerr ("%s\n", error_local->message);
continue;
}
str = fu_firmware_to_string (firmware);
g_print ("%s", str);
any_okay = TRUE;
}
if (!any_okay) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to parse");
return FALSE;
}
return TRUE;
}
static void
fu_firmware_dump_log_cb (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer user_data)
{
FuUtil *self = (FuUtil *) user_data;
if (log_level == G_LOG_LEVEL_CRITICAL) {
g_printerr ("CRITICAL: %s\n", message);
g_assert_not_reached ();
}
if (self->verbose)
g_printerr ("DEBUG: %s\n", message);
}
static void
fu_util_private_free (FuUtil *self)
{
if (self->array != NULL)
g_ptr_array_unref (self->array);
if (self->engine != NULL)
g_object_unref (self->engine);
g_free (self);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtil, fu_util_private_free)
#pragma clang diagnostic pop
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
g_autofree gchar *str = NULL;
g_autoptr(FuFirmware) firmware = NULL;
g_autoptr(GBytes) blob = NULL;
g_autoptr(GError) error = NULL; g_autoptr(GError) error = NULL;
gsize sz = 0; g_autoptr(GPtrArray) firmware_types = NULL;
const guint8 *buf; g_autoptr(GOptionContext) context = NULL;
g_autoptr(FuUtil) self = g_new0 (FuUtil, 1);
const GOptionEntry options[] = {
{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &self->verbose,
/* TRANSLATORS: command line option */
_("Show extra debugging information"), NULL },
{ "timeout", 't', 0, G_OPTION_ARG_INT, &self->timeout,
/* TRANSLATORS: command line option */
_("Timeout in milliseconds for each parse"), NULL },
{ NULL}
};
/* no args */ setlocale (LC_ALL, "");
if (argc != 2) {
g_printerr ("firmware filename required\n"); bindtextdomain (GETTEXT_PACKAGE, FWUPD_LOCALEDIR);
return 2; bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
textdomain (GETTEXT_PACKAGE);
context = g_option_context_new (NULL);
g_option_context_add_main_entries (context, options, NULL);
if (!g_option_context_parse (context, &argc, &argv, &error)) {
/* TRANSLATORS: the user didn't read the man page */
g_printerr ("%s: %s\n", _("Failed to parse arguments"),
error->message);
return EXIT_FAILURE;
} }
/* load firmware */ /* args */
blob = fu_common_get_contents_bytes (argv[1], &error); if (self->verbose) {
if (blob == NULL) { g_setenv ("G_MESSAGES_DEBUG", "all", FALSE);
g_printerr ("failed to load file: %s\n", error->message); g_setenv ("FWUPD_VERBOSE", "1", FALSE);
}
/* crashy mccrash face */
g_log_set_default_handler (fu_firmware_dump_log_cb, self);
/* load engine */
self->engine = fu_engine_new (FU_APP_FLAGS_NO_IDLE_SOURCES);
if (!fu_engine_load (self->engine, FU_ENGINE_LOAD_FLAG_READONLY, &error)) {
g_printerr ("Failed to load engine: %s\n", error->message);
return 1; return 1;
} }
buf = g_bytes_get_data (blob, &sz);
if (sz < 2) { /* get all parser objects */
g_printerr ("firmware invalid\n"); firmware_types = fu_engine_get_firmware_gtype_ids (self->engine);
return 2; self->array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
for (guint i = 0; i < firmware_types->len; i++) {
const gchar *id = g_ptr_array_index (firmware_types, i);
GType gtype = fu_engine_get_firmware_gtype_by_id (self->engine, id);
g_ptr_array_add (self->array, g_object_new (gtype, NULL));
} }
if (buf[0] == 'S' && buf[1] == '0') {
firmware = fu_srec_firmware_new (); /* no args */
} else if (buf[0] == ':') { if (argc >= 2) {
firmware = fu_ihex_firmware_new (); gint rc = 0;
} else { for (gint i = 1; i < argc; i++) {
g_printerr ("firmware invalid type, expected .srec or .hex\n"); g_autoptr(GBytes) blob = NULL;
return 2; g_autoptr(GError) error_local = NULL;
blob = fu_common_get_contents_bytes (argv[i], &error_local);
if (blob == NULL) {
g_printerr ("failed to load file %s: %s\n",
argv[i], error_local->message);
rc = 2;
continue;
}
if (!fu_firmware_dump_iter (self, blob, &error_local)) {
g_printerr ("failed to parse file %s: %s\n",
argv[i], error_local->message);
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT)) {
return 4;
}
rc = 3;
continue;
}
}
return rc;
} }
if (!fu_firmware_parse (firmware, blob, for (;;) {
FWUPD_INSTALL_FLAG_IGNORE_VID_PID | gsize len = 0;
FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM, guint8 *buf = NULL;
&error)) { g_autoptr(GBytes) blob = NULL;
g_printerr ("failed to parse file: %s\n", error->message); #ifdef HAVE_HF_ITER
return 3; HF_ITER(&buf, &len);
#endif
blob = g_bytes_new_static (buf, len);
for (guint i = 0; i < self->array->len; i++) {
FuFirmware *firmware = g_ptr_array_index (self->array, i);
g_autoptr(GError) error_local = NULL;
if (!fu_firmware_dump_parse (self, firmware, blob, &error_local)) {
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT)) {
g_error ("%s", error_local->message);
}
g_assert (error_local != NULL);
continue;
}
}
} }
str = fu_firmware_to_string (firmware);
g_print ("%s", str);
return 0; return 0;
} }

Binary file not shown.

View File

@ -0,0 +1,18 @@
<firmware gtype="FuBcm57xxFirmware">
<version>1.2.3</version>
<image gtype="FuBcm57xxStage1Image">
<version>7.8.9</version>
<id>stage1</id>
<idx>0x01</idx>
<filename>bcm57xx-stage1.bin</filename>
</image>
<image gtype="FuBcm57xxStage2Image">
<id>stage2</id>
<data/> <!-- empty! -->
</image>
<image gtype="FuBcm57xxDictImage">
<id>ape</id>
<addr>0x7</addr>
<data>aGVsbG8gd29ybGQ=</data> <!-- base64 -->
</image>
</firmware>

View File

@ -0,0 +1,19 @@
<firmware gtype="FuCrosEcFirmware">
<base>0x3000</base>
<image>
<id>RO_FRID</id>
<data>Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA=</data>
</image>
<image>
<id>RW_FWID</id>
<data>Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA=</data>
</image>
<image>
<id>EC_RO</id>
<data>Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA=</data>
</image>
<image>
<id>EC_RW</id>
<data>Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA=</data>
</image>
</firmware>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
{"firmware": "OjA0NDAwMDAwM0RFRjIwRjA4MAo6MDAwMDAwMDFGRgo=", "signature": "aGVsbG8gd29ybGQ="}

Binary file not shown.

View File

@ -0,0 +1,9 @@
WACOM2080080000009414E080400000009414A3F
WA10800800067
S00B00004F616B2E73726563FE
S70508038BD58F
WA2080400001F
S00B00004F616B2E73726563FE
S70508070BD50B

View File

@ -0,0 +1,10 @@
<firmware gtype="FuFmapFirmware">
<image>
<id>FMAP</id>
<data>aGVsbG8gd29ybGQ=</data>
</image>
<image>
<id>TEST</id>
<data>V29ybGQh</data>
</image>
</firmware>

View File

@ -1,17 +1,25 @@
run_target('fuzz-smbios', if honggfuzz.found()
command: [ run_target('fuzz-smbios',
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'), command: [
'-i', join_paths(meson.current_source_dir(), 'smbios'), honggfuzz,
'-o', join_paths(meson.current_build_dir(), '..', 'findings-smbios'), '--input', join_paths(meson.current_source_dir(), 'smbios'),
'--command', 'smbios-dump', '--output', join_paths(meson.current_build_dir(), 'smbios-corpus'),
fwupdtool, '--workspace', join_paths(meson.current_build_dir(), 'smbios-findings'),
], '--verifier',
) '--rlimit_rss', '10',
run_target('fuzz-firmware', '--', fwupdtool, 'smbios-dump', '___FILE___',
command: [ ],
join_paths(meson.source_root(), 'contrib/afl-fuzz.py'), )
'-i', join_paths(meson.current_source_dir(), 'firmware'), run_target('fuzz-firmware',
'-o', join_paths(meson.current_build_dir(), '..', 'findings-firmware'), command: [
fwupd_firmware_dump, honggfuzz,
], '--input', join_paths(meson.current_source_dir(), 'firmware'),
'--output', join_paths(meson.current_build_dir(), 'firmware-corpus'),
'--workspace', join_paths(meson.current_build_dir(), 'firmware-findings'),
'--verifier',
'--rlimit_rss', '10',
'--timeout', '5', '-P', '--',
fwupd_firmware_dump, '--timeout', '50',
],
) )
endif

View File

@ -0,0 +1,7 @@
<firmware gtype="FuSynapromFirmware">
<version>1.2</version>
<image>
<product_id>0x42</product_id>
<data>aGVsbG8gd29ybGQ=</data> <!-- base64 -->
</image>
</firmware>

View File

@ -307,14 +307,13 @@ if get_option('tests')
], ],
) )
test('fu-self-test', e, is_parallel:false, timeout:180) test('fu-self-test', e, is_parallel:false, timeout:180)
endif
if get_option('tests')
# for fuzzing # for fuzzing
fwupd_firmware_dump = executable( fwupd_firmware_dump = executable(
'fwupd-firmware-dump', 'fwupd-firmware-dump',
sources : [ sources : [
'fu-firmware-dump.c', 'fu-firmware-dump.c',
daemon_src,
], ],
include_directories : [ include_directories : [
root_incdir, root_incdir,
@ -322,8 +321,7 @@ if get_option('tests')
fwupdplugin_incdir, fwupdplugin_incdir,
], ],
dependencies : [ dependencies : [
libxmlb, daemon_dep,
gio,
], ],
link_with : [ link_with : [
fwupd, fwupd,
@ -332,6 +330,4 @@ if get_option('tests')
) )
endif endif
if get_option('tests') subdir('fuzzing')
subdir('fuzzing')
endif