Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit of a fuzzer for Meshtastic #5790

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
39 changes: 39 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# trunk-ignore-all(trivy/DS026): No healthcheck is needed for this builder container
# trunk-ignore-all(trivy/DS002): We must run as root for this container
# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container
# trunk-ignore-all(hadolint/DL3002): We must run as root for this container

FROM gcr.io/oss-fuzz-base/base-builder:v1

ENV PIP_ROOT_USER_ACTION=ignore

# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir -U \
platformio==6.1.16 \
grpcio-tools==1.68.1 \
meshtastic==2.5.9

# Ugly hack to avoid clang detecting a conflict between the math "log" function and the "log" function in framework-portduino/cores/portduino/logging.h
RUN sed -i -e 's/__MATHCALL_VEC (log,, (_Mdouble_ __x));//' /usr/include/x86_64-linux-gnu/bits/mathcalls.h

# A few dependencies are too old on the base-builder image. More recent versions are built from source.
WORKDIR $SRC
RUN git config --global advice.detachedHead false && \
git clone --depth 1 --branch 0.8.0 https://github.com/jbeder/yaml-cpp.git && \
git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania.git && \
git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder.git && \
git clone --depth 1 --branch v2.7.15 https://github.com/babelouest/ulfius.git

COPY ./.clusterfuzzlite/build.sh $SRC/

WORKDIR $SRC/firmware
COPY . $SRC/firmware/

ENV PIO_ENV=buildroot
RUN platformio pkg install --environment $PIO_ENV
59 changes: 59 additions & 0 deletions .clusterfuzzlite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ClusterFuzzLite for Meshtastic

This directory contains the fuzzer implementation for Meshtastic using the ClusterFuzzLite framework.
See the [ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/) for more details.

## Running locally

ClusterFuzzLite uses the OSS-Fuzz toolchain. To build the fuzzer manually, first grab a copy of OSS-Fuzz.

```shell
git clone https://github.com/google/oss-fuzz.git
cd oss-fuzz
```

To build the fuzzer, run:

```shell
python3 infra/helper.py build_image --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY
python3 infra/helper.py build_fuzzers --external $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY --sanitizer address
```

To run the fuzzer, run:

```shell
python3 infra/helper.py run_fuzzer --external --corpus-dir=<path-to-temp-corpus-dir> $PATH_TO_MESHTASTIC_FIRMWARE_DIRECTORY router_fuzzer
```

More background on these commands can be found in the
[ClusterFuzzLite documentation](https://google.github.io/clusterfuzzlite/build-integration/#testing-locally).

## router_fuzzer.cpp

This fuzzer submits MeshPacket protos to the `Router::enqueueReceivedMessage` method. It takes the binary
data from the fuzzer and decodes that data to a MeshPacket using nanopb. A few fields in
the MeshPacket are modified by the fuzzer.

- If the `to` field is 0, it will be replaced with the NodeID of the running node.
- If the `from` field is 0, it will be replaced with the NodeID of the running node.
- If the `id` field is 0, it will be replaced with an incrementing counter value.
- If the `pki_encrypted` field is true, the `public_key` field will be populated with the first admin key.

The `router_fuzzer_seed_corpus.py` file contains a list of MeshPackets. It is run from inside build.sh and
writes the binary MeshPacket protos to files. These files are use used by the fuzzer as its initial seed data,
helping the fuzzer to start off with a few known inputs.

### Interpreting a fuzzer crash

If the fuzzer crashes, it'll write the input bytes used for the test case to a file and notify about the
location of that file. The contents of the file are a binary serialized MeshPacket protobuf. The following
snippet of Python code can be used to parse the file into a human readable form.

```python
from meshtastic.protobuf import mesh_pb2

mesh_pb2.MeshPacket.FromString(open("crash-XXXX-file", "rb").read())
```

Consider adding any such crash results to the `router_fuzzer_seed_corpus.py` file to ensure there a isn't
a future regression for that crash test case.
67 changes: 67 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash -eu

# Build Mestastic and a few needed dependencies using clang++
# and the OSS-Fuzz required build flags.

env

cd "$SRC"

LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$SRC/yaml-cpp/build" \
-DBUILD_SHARED_LIBS=OFF
cmake --build "$SRC/yaml-cpp/build" -j "$(nproc)"
cmake --install "$SRC/yaml-cpp/build" --prefix /usr

cmake -S "$SRC/orcania" -B "$SRC/orcania/build" \
-DBUILD_STATIC=ON
cmake --build "$SRC/orcania/build" -j "$(nproc)"
cmake --install "$SRC/orcania/build" --prefix /usr

cmake -S "$SRC/yder" -B "$SRC/yder/build" \
-DBUILD_STATIC=ON -DWITH_JOURNALD=OFF
cmake --build "$SRC/yder/build" -j "$(nproc)"
cmake --install "$SRC/yder/build" --prefix /usr

cmake -S "$SRC/ulfius" -B "$SRC/ulfius/build" \
-DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF
cmake --build "$SRC/ulfius/build" -j "$(nproc)"
cmake --install "$SRC/ulfius/build" --prefix /usr

cd "$SRC/firmware"

PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
export PLATFORMIO_EXTRA_SCRIPTS
export STATIC_LIBS
export TARGET_CC=$CC
export TARGET_CXX=$CXX
export TARGET_LD=$CXX
export TARGET_AR=llvm-ar
export TARGET_AS=llvm-as
export TARGET_OBJCOPY=llvm-objcopy
export TARGET_RANLIB=llvm-ranlib

mkdir -p "$OUT/lib"

cp .clusterfuzzlite/*_fuzzer.options "$OUT/"

for f in .clusterfuzzlite/*_fuzzer.cpp; do
fuzzer=$(basename "$f" .cpp)
cp -f "$f" src/fuzzer.cpp
pio run -vvv --environment "$PIO_ENV"
cp ".pio/build/$PIO_ENV/program" "$OUT/$fuzzer"

# Copy shared libraries used by the fuzzer.
cp -f $(ldd ".pio/build/$PIO_ENV/program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') "$OUT/lib/"

# Build the initial fuzzer seed corpus.
corpus_name="${fuzzer}_seed_corpus"
corpus_generator="$PWD/.clusterfuzzlite/${corpus_name}.py"
if [[ -f $corpus_generator ]]; then
mkdir "$corpus_name"
pushd "$corpus_name"
python3 "$corpus_generator"
popd
zip -D "$OUT/${corpus_name}.zip" "$corpus_name"/*
fi
done
35 changes: 35 additions & 0 deletions .clusterfuzzlite/platformio-clusterfuzzlite-post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""PlatformIO build script (post: runs after other Meshtastic scripts)."""

import os
import shlex

from SCons.Script import DefaultEnvironment

env = DefaultEnvironment()

# Remove any static libraries from the LIBS environment. Static libraries are
# handled in platformio-clusterfuzzlite-pre.py.
static_libs = set(lib[2:] for lib in shlex.split(os.getenv("STATIC_LIBS")))
env.Replace(
LIBS=[
lib for lib in env["LIBS"] if not (isinstance(lib, str) and lib in static_libs)
],
)

# FrameworkArduino/portduino/main.cpp contains the "main" function the binary.
# The fuzzing framework also provides a "main" function and needs to be run
# before Meshtastic is started. We rename the "main" function for Meshtastic to
# "portduino_main" here so that it can be called inside the fuzzer.
env.AddPostAction(
"$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o",
env.VerboseAction(
" ".join(
[
"$OBJCOPY",
"--redefine-sym=main=portduino_main",
"$BUILD_DIR/FrameworkArduino/portduino/main.cpp.o",
]
),
"Renaming main symbol to portduino_main",
),
)
48 changes: 48 additions & 0 deletions .clusterfuzzlite/platformio-clusterfuzzlite-pre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""PlatformIO build script (pre: runs before other Meshtastic scripts).

ClusterFuzzLite executes in a different container from the build. During the build,
attempt to link statically to as many dependencies as possible. For dependencies that
do not have static libraries, the shared library files are copied to the output
directory by the build.sh script.
"""

import glob
import os
import shlex

from SCons.Script import DefaultEnvironment, Literal

env = DefaultEnvironment()

sanitizer_flags = shlex.split(os.getenv("SANITIZER_FLAGS"))
lib_fuzzing_engine = shlex.split(os.getenv("LIB_FUZZING_ENGINE"))
statics = glob.glob("/usr/lib/lib*.a") + glob.glob("/usr/lib/*/lib*.a")
no_static = set(("-ldl",))


def replaceStatic(lib):
"""Replace -l<libname> with the static .a file for the library."""
if not lib.startswith("-l") or lib in no_static:
return lib
static_name = f"/lib{lib[2:]}.a"
static = [s for s in statics if s.endswith(static_name)]
if len(static) == 1:
return static[0]
return lib


# Setup the environment for building with Clang and the OSS-Fuzz required build flags.
env.Append(
CFLAGS=os.getenv("CFLAGS"),
CXXFLAGS=os.getenv("CXXFLAGS"),
LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"],
LINKFLAGS=sanitizer_flags + lib_fuzzing_engine + ["-stdlib=libc++", "-std=c++17"],
_LIBFLAGS=[replaceStatic(s) for s in shlex.split(os.getenv("STATIC_LIBS"))]
+ [
"/usr/lib/x86_64-linux-gnu/libunistring.a", # Needs to be at the end.
# Find the shared libraries in a subdirectory named lib
# within the same directory as the binary.
Literal("-Wl,-rpath,$ORIGIN/lib"),
"-Wl,-z,origin",
],
)
1 change: 1 addition & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: c++
Loading
Loading