fuzz: Add fuzzing for core and i2c

Add fuzzing infrastructure and a fuzz target handling mctp-i2c packets.
After running for a few days with honggfuzz this achieves close to
complete line coverage of core.c

Change-Id: I6cd7361e6f600831a319f06fb7c7c0d2186fd7de
Signed-off-by: Matt Johnston <matt@codeconstruct.com.au>
diff --git a/docs/fuzzing.md b/docs/fuzzing.md
new file mode 100644
index 0000000..16beb7a
--- /dev/null
+++ b/docs/fuzzing.md
@@ -0,0 +1,80 @@
+# Fuzzing libmctp
+
+## Build
+
+From the top level libmctp directory, run `./tests/fuzz/fuzz-build.py`. That
+will produce several build variants required for different fuzz engines/stages.
+
+## Honggfuzz
+
+[Honggfuzz](https://github.com/google/honggfuzz) handles running across multiple
+threads itself with a single corpus directory, which is easy to work with. It
+needs to be built from source.
+
+Run with
+
+```
+nice honggfuzz -T -i corpusdir --linux_perf_branch -- ./bhf/tests/fuzz/i2c-fuzz
+```
+
+The `--linux_perf_branch` switch is optional, it requires permissions for perf
+counters:
+
+```
+echo 0 | sudo tee /proc/sys/kernel/perf_event_paranoid
+```
+
+Optionally a thread count can be given, 24 threads on a 12 core system seems to
+give best utilisation (`--threads 24`).
+
+The corpus directory can be reused between runs with different fuzzers.
+
+## AFL++
+
+Running a single instance (just for testing):
+
+```
+afl-fuzz -i fuzzrun/hf11/ -o fuzzrun/out12single ./bfuzz/tests/fuzz/i2c-fuzz
+```
+
+AFL++ requires a separate TUI instantiation for each CPU thread. The helper
+[AFL Runner](https://github.com/0xricksanchez/afl_runner) makes that easier.
+
+Running with 20 threads:
+
+```
+nice aflr run  -t bfuzz/tests/fuzz/i2c-fuzz -i workdir/out5/m_i2c-fuzz/queue -o workdir/out6 -c bcmplog/tests/fuzz/i2c-fuzz -s bfuzzasan/tests/fuzz/i2c-fuzz -n 20  --session-name fuzz
+```
+
+Kill it with `aflr kill fuzz`.
+
+`aflr tui workdir/out6` could be used to view progress, though its calculations
+may be inaccurate if some runners are idle. Another option is
+`afl-whatsup workdir/out6`.
+
+## Coverage
+
+The coverage provided by a corpus directory can be reported using
+`tests/fuzz/fuzz-coverage.py`.
+
+It will:
+
+- Run a binary compiled with `--coverage` against each corpus file
+- Use [grcov](https://github.com/mozilla/grcov) to aggregate the coverage traces
+  (much faster than lcov).
+- Use `genhtml` to create a report
+
+Typical usage, with corpus in `fuzzrun/corpus`:
+
+```
+./tests/fuzz/fuzz-coverage.py fuzzrun/corpus bnoopt/tests/fuzz/i2c-fuzz . bnoopt/ coverage-output
+```
+
+## Reproducing crashes
+
+When the fuzz run encounters a crash, the testcase can be run against the built
+target manually, and stepped through with GDB etc.
+
+```
+./bnoopt/tests/fuzz/i2c-fuzz < crashing.bin
+```
diff --git a/tests/fuzz/fuzz-build.py b/tests/fuzz/fuzz-build.py
new file mode 100755
index 0000000..82edfb1
--- /dev/null
+++ b/tests/fuzz/fuzz-build.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+# Builds fuzzing variants. Run this from the toplevel directory.
+# Beware this will wipe build directories.
+
+# Requires honggfuzz and afl++ installed
+
+# Builds are:
+# * AFL (normal, asan, cmplog)
+# * honggfuzz (asan, msan, ubsan)
+# * -O0, with coverage
+
+import os
+import subprocess
+
+# reduce warning level since tests since gtest is noisy
+BASE_MESONFLAGS = "-Dwarning_level=2 -Ddefault_library=static --wipe".split()
+FUZZ_PROGRAMS = ["tests/fuzz/i2c-fuzz"]
+
+
+def build(
+    build_dir: str,
+    cc: str = None,
+    cxx: str = None,
+    cflags="",
+    cxxflags="",
+    opt="3",
+    env={},
+    mesonflags=[],
+):
+    env = os.environ | env
+    env["CFLAGS"] = cflags
+    env["CXXFLAGS"] = cxxflags
+
+    # Meson sets CC="ccache cc" by default, but ccache removes -fprofile-arcs
+    # so coverage breaks (ccache #1531). Prevent that by setting CC/CXX.
+    env["CC"] = cc if cc else "cc"
+    env["CXX"] = cxx if cxx else "c++"
+
+    meson_cmd = ["meson"] + BASE_MESONFLAGS + mesonflags
+    meson_cmd += [f"-Doptimization={opt}"]
+    meson_cmd += [build_dir]
+    subprocess.run(meson_cmd, env=env, check=True)
+
+    ninja_cmd = ["ninja", "-C", build_dir] + FUZZ_PROGRAMS
+    subprocess.run(ninja_cmd, env=env, check=True)
+
+
+def build_afl():
+    env = {
+        # seems to be required for afl-clang-lto?
+        "AFL_REAL_LD": "ld.lld",
+    }
+    cc = "afl-clang-lto"
+    cxx = "afl-clang-lto++"
+
+    # normal
+    build("bfuzz", cc=cc, cxx=cxx, env=env)
+    # ASAN
+    build(
+        "bfuzzasan",
+        cc=cc,
+        cxx=cxx,
+        mesonflags=["-Db_sanitize=address"],
+        env=env,
+    )
+    # cmplog
+    build("bcmplog", cc=cc, cxx=cxx, env={"AFL_LLVM_CMPLOG": "1"} | env)
+
+
+def main():
+    # No profiling, has coverage
+    build(
+        "bnoopt",
+        cflags="-fprofile-abs-path",
+        cxxflags="-fprofile-abs-path",
+        opt="0",
+        mesonflags=["-Db_coverage=true"],
+    )
+
+    # AFL
+    build_afl()
+
+    # Honggfuzz
+    # asan by default
+    build(
+        "bhf",
+        cc="hfuzz-clang",
+        cxx="hfuzz-clang++",
+        env={"HFUZZ_CC_ASAN": "1"},
+    )
+    # msan
+    build(
+        "bhf-msan",
+        cc="hfuzz-clang",
+        cxx="hfuzz-clang++",
+        env={"HFUZZ_CC_MSAN": "1"},
+    )
+    # ubsan
+    build(
+        "bhf-ubsan",
+        cc="hfuzz-clang",
+        cxx="hfuzz-clang++",
+        env={"HFUZZ_CC_UBSAN": "1"},
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/fuzz/fuzz-coverage.py b/tests/fuzz/fuzz-coverage.py
new file mode 100755
index 0000000..9df4a5e
--- /dev/null
+++ b/tests/fuzz/fuzz-coverage.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+# usage: fuzz-coverage.py [-h] corpus program srcdir builddir outdir
+
+# Runs corpus (directory of testcases) against a program
+# built with coverage, and produces a html report.
+
+# The program should be built with --coverage -fprofile-abs-path
+# -O0 may make the html report more legible?
+
+# Requires lcov and https://github.com/mozilla/grcov
+
+import argparse
+import subprocess
+import sys
+from pathlib import Path
+
+
+def run(args):
+    corpus = Path(args.corpus)
+    outdir = Path(args.outdir)
+
+    for c in Path(args.builddir).glob("**/*.gcda"):
+        print(f"Removed old coverage {c}", file=sys.stderr)
+        c.unlink()
+
+    print("Running corpus", file=sys.stderr)
+    for c in corpus.glob("*"):
+        c = c.open("rb").read()
+        subprocess.run([args.program], input=c)
+
+    print("Running grcov", file=sys.stderr)
+    outdir.mkdir(parents=True, exist_ok=True)
+    coverage_paths = [args.builddir]
+    lcov_file = outdir / "lcov.info"
+
+    subprocess.run(
+        [
+            "grcov",
+            "-b",
+            args.program,
+            "-o",
+            lcov_file,
+            "-t",
+            "lcov",
+            "-s",
+            args.srcdir,
+        ]
+        + coverage_paths,
+        check=True,
+    )
+
+    print("Running genhtml", file=sys.stderr)
+    subprocess.run(
+        [
+            "genhtml",
+            "-o",
+            outdir,
+            "--show-details",
+            "--highlight",
+            "--ignore-errors",
+            "source",
+            "--ignore-errors",
+            "unmapped",
+            "--legend",
+            lcov_file,
+        ],
+        check=True,
+    )
+
+    html = outdir / "index.html"
+    print(f"\n\nOutput is file://{html.absolute()}", file=sys.stderr)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("corpus", type=str, help="Corpus directory")
+    parser.add_argument("program", type=str, help="Target Program")
+    parser.add_argument("srcdir", type=str, help="Source directory")
+    parser.add_argument("builddir", type=str)
+    parser.add_argument("outdir", type=str)
+    args = parser.parse_args()
+
+    run(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/fuzz/i2c-fuzz.c b/tests/fuzz/i2c-fuzz.c
new file mode 100644
index 0000000..4179ebe
--- /dev/null
+++ b/tests/fuzz/i2c-fuzz.c
@@ -0,0 +1,311 @@
+#include <stdbool.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <errno.h>
+#include <endian.h>
+
+#include "compiler.h"
+#include "libmctp.h"
+#include "libmctp-i2c.h"
+#include "libmctp-sizes.h"
+#include "libmctp-alloc.h"
+
+#if NDEBUG
+static_assert(0, "fuzzing shouldn't build with NDEBUG");
+#endif
+
+/* Limits memory used in tx path */
+#define MAX_SEND 600
+
+/* Avoids wasting time traversing unreachable sizes */
+#define MAX_RECEIVE 30
+
+static const size_t FUZZCTRL_SIZE = 0x400;
+
+static const uint8_t RX_CHANCE = 90;
+static const uint8_t TX_BUSY_CHANCE = 3;
+
+static const uint8_t OWN_I2C_ADDR = 0x20;
+static const uint8_t OWN_EID = 123;
+
+/* time step in milliseconds */
+static const uint32_t MAX_TIME_STEP = 15000;
+
+struct fuzz_buf {
+	size_t len;
+	size_t pos;
+	const uint8_t *data;
+};
+
+struct fuzz_ctx {
+	struct fuzz_buf *ctrl;
+	struct fuzz_buf *input;
+
+	struct mctp_binding_i2c *i2c;
+	struct mctp *mctp;
+
+	uint64_t now;
+
+	bool done;
+};
+
+static struct fuzz_buf *fuzz_buf_new(const void *data, size_t len)
+{
+	struct fuzz_buf *buf = malloc(sizeof(struct fuzz_buf));
+	buf->pos = 0;
+	buf->len = len;
+	buf->data = data;
+	return buf;
+}
+
+static const void *fuzz_buf_extract(struct fuzz_buf *buf, size_t len)
+{
+	if (buf->pos + len > buf->len) {
+		return NULL;
+	}
+
+	const void *ret = &buf->data[buf->pos];
+	buf->pos += len;
+	return ret;
+}
+
+/* Returns true on success */
+static bool fuzz_buf_extract_u32(struct fuzz_buf *buf, uint32_t *ret)
+{
+	const void *r = fuzz_buf_extract(buf, sizeof(uint32_t));
+	if (!r) {
+		return false;
+	}
+
+	uint32_t v;
+	memcpy(&v, r, sizeof(v));
+	*ret = be32toh(v);
+	return true;
+}
+
+/* Returns true with roughly `percent` chance */
+static bool fuzz_chance(struct fuzz_ctx *ctx, uint8_t percent)
+{
+	assert(percent <= 100);
+
+	const uint8_t *v = fuzz_buf_extract(ctx->ctrl, sizeof(uint8_t));
+	if (!v) {
+		return false;
+	}
+
+	uint8_t cutoff = (uint32_t)percent * UINT8_MAX / 100;
+	return *v <= cutoff;
+}
+
+static int fuzz_i2c_tx(const void *buf, size_t len, void *c)
+{
+	struct fuzz_ctx *ctx = c;
+	(void)buf;
+	(void)len;
+
+	if (fuzz_chance(ctx, TX_BUSY_CHANCE)) {
+		return -EBUSY;
+	}
+
+	return 0;
+}
+
+static void fuzz_i2c_rxmsg(uint8_t src_eid, bool tag_owner, uint8_t msg_tag,
+			   void *c, void *msg, size_t len)
+{
+	struct fuzz_ctx *ctx = c;
+	(void)ctx;
+	(void)src_eid;
+	(void)tag_owner;
+	(void)msg_tag;
+	(void)msg;
+	(void)len;
+}
+
+static void do_rx(struct fuzz_ctx *ctx)
+{
+	uint32_t len;
+	if (!fuzz_buf_extract_u32(ctx->ctrl, &len)) {
+		ctx->done = true;
+		return;
+	}
+
+	if (len > MAX_RECEIVE) {
+		ctx->done = true;
+		return;
+	}
+
+	const uint8_t *data = fuzz_buf_extract(ctx->input, len);
+	if (!data) {
+		ctx->done = true;
+		return;
+	}
+
+	mctp_i2c_rx(ctx->i2c, data, len);
+}
+
+static void do_tx(struct fuzz_ctx *ctx)
+{
+	int rc;
+
+	const uint8_t *e = fuzz_buf_extract(ctx->ctrl, sizeof(uint8_t));
+	if (!e) {
+		ctx->done = true;
+		return;
+	}
+	mctp_eid_t eid = *e;
+
+	bool tag_owner = fuzz_chance(ctx, 50);
+	/* `t` generates the dest eid in owner case, or tag in non-owner case */
+	const uint8_t *t = fuzz_buf_extract(ctx->ctrl, sizeof(uint8_t));
+	if (!t) {
+		ctx->done = true;
+		return;
+	}
+
+	uint32_t len;
+	if (!fuzz_buf_extract_u32(ctx->ctrl, &len)) {
+		ctx->done = true;
+		return;
+	}
+	len = len % (MAX_SEND + 1);
+
+	uint8_t *fake_send_data = __mctp_msg_alloc(len, ctx->mctp);
+
+	mctp_i2c_tx_poll(ctx->i2c);
+
+	if (tag_owner) {
+		/* Random destination from a small set, reuse `t` */
+		mctp_eid_t dest = 10 + (*t % 5);
+		uint8_t tag;
+		rc = mctp_message_tx_request(ctx->mctp, dest, fake_send_data,
+					     len, &tag);
+		if (rc == 0) {
+			assert((tag & MCTP_HDR_TAG_MASK) == tag);
+		}
+	} else {
+		uint8_t tag = *t % 8;
+		mctp_message_tx_alloced(ctx->mctp, eid, tag_owner, tag,
+					fake_send_data, len);
+	}
+}
+
+static uint64_t fuzz_now(void *c)
+{
+	struct fuzz_ctx *ctx = c;
+
+	uint32_t step = 10;
+	uint32_t s;
+	if (fuzz_buf_extract_u32(ctx->ctrl, &s)) {
+		step = s % (MAX_TIME_STEP + 1);
+	}
+
+	uint64_t prev = ctx->now;
+	ctx->now += step;
+	/* Notice if overflow occurs */
+	assert(ctx->now >= prev);
+	return ctx->now;
+}
+
+int LLVMFuzzerTestOneInput(uint8_t *input, size_t len)
+{
+	/* Split input into two parts. First FUZZCTRL_SIZE (0x400 bytes currently)
+     * is used for fuzzing control (random choices etc).
+     * The remainder is a PLDM packet stream, of length:data */
+	if (len < FUZZCTRL_SIZE) {
+		return 0;
+	}
+
+	struct fuzz_ctx _ctx = {
+		.ctrl = fuzz_buf_new(input, FUZZCTRL_SIZE),
+		.input = fuzz_buf_new(&input[FUZZCTRL_SIZE],
+				      len - FUZZCTRL_SIZE),
+		.now = 0,
+		.done = false,
+	};
+	struct fuzz_ctx *ctx = &_ctx;
+
+	/* Instantiate the MCTP stack */
+	ctx->i2c = malloc(MCTP_SIZEOF_BINDING_I2C);
+	mctp_i2c_setup(ctx->i2c, OWN_I2C_ADDR, fuzz_i2c_tx, ctx);
+	ctx->mctp = mctp_init();
+	mctp_register_bus(ctx->mctp, mctp_binding_i2c_core(ctx->i2c), OWN_EID);
+	mctp_set_rx_all(ctx->mctp, fuzz_i2c_rxmsg, ctx);
+	mctp_set_now_op(ctx->mctp, fuzz_now, ctx);
+
+	while (!ctx->done) {
+		if (fuzz_chance(ctx, RX_CHANCE)) {
+			do_rx(ctx);
+		} else {
+			do_tx(ctx);
+		}
+	}
+
+	mctp_destroy(ctx->mctp);
+	free(ctx->i2c);
+	free(ctx->ctrl);
+	free(ctx->input);
+
+	return 0;
+}
+
+int LLVMFuzzerInitialize(int *argc __unused, char ***argv __unused)
+{
+	return 0;
+}
+
+#ifdef HFND_FUZZING_ENTRY_FUNCTION
+#define USING_HONGGFUZZ 1
+#else
+#define USING_HONGGFUZZ 0
+#endif
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+#define USING_AFL 1
+#else
+#define USING_AFL 0
+#endif
+
+#if USING_AFL
+__AFL_FUZZ_INIT();
+#endif
+
+#if !USING_AFL && !USING_HONGGFUZZ
+/* Let it build without AFL taking stdin instead */
+static void run_standalone()
+{
+	while (true) {
+		unsigned char buf[1024000];
+		ssize_t len = read(STDIN_FILENO, buf, sizeof(buf));
+		if (len <= 0) {
+			break;
+		}
+		LLVMFuzzerTestOneInput(buf, len);
+	}
+}
+#endif
+
+#if !USING_HONGGFUZZ
+int main(int argc, char **argv)
+{
+	LLVMFuzzerInitialize(&argc, &argv);
+
+#if USING_AFL
+	__AFL_INIT();
+	uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
+
+	while (__AFL_LOOP(100000)) {
+		size_t len = __AFL_FUZZ_TESTCASE_LEN;
+		LLVMFuzzerTestOneInput(buf, len);
+	}
+#else
+	run_standalone();
+#endif
+
+	return 0;
+}
+#endif // !USING_HONGGFUZZ
diff --git a/tests/fuzz/meson.build b/tests/fuzz/meson.build
new file mode 100644
index 0000000..b4a51c6
--- /dev/null
+++ b/tests/fuzz/meson.build
@@ -0,0 +1,10 @@
+if get_option('bindings').contains('i2c')
+    executable(
+        'i2c-fuzz',
+        'i2c-fuzz.c',
+        # for __AFL_LOOP
+        cpp_args: ['-Wno-gnu-statement-expression-from-macro-expansion'],
+        include_directories: test_include_dirs,
+        dependencies: [libmctp_dep],
+        )
+endif
diff --git a/tests/meson.build b/tests/meson.build
index 96d7d6b..eb398b0 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -29,3 +29,5 @@
         ),
     )
 endforeach
+
+subdir('fuzz')