mirror of
https://github.com/meshtastic/firmware.git
synced 2025-02-01 02:09:57 +00:00
Initial commit of a fuzzer for Meshtastic (#5790)
* Initial commit of a fuzzer for Meshtastic. * Use a max of 5 for the phone queues * Only write files to the temp dir * Limitless queue + fuzzer = lots of ram :) * Use $PIO_ENV for path to program * spelling: s/is/to/ * Use loopCanSleep instead of a lock in Router * realHardware allows full use of a CPU core * Ignore checkov CKV_DOCKER_2 & CKV_DOCKER_3 * Add Atak seed * Fix lint issues in build.sh * Use exception to exit from portduino_main * Separate build & source files into $WORK & $SRC * Use an ephemeral port for the API server * Include CXXFLAGS in the link step * Read all shared libraries * Use a separate work directory for each sanitizer --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
parent
f132158c3e
commit
b0fe5ef8ba
52
.clusterfuzzlite/Dockerfile
Normal file
52
.clusterfuzzlite/Dockerfile
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# This container is used to build Meshtastic with the libraries required by the fuzzer.
|
||||||
|
# ClusterFuzzLite starts the container, runs the build.sh script, and then exits.
|
||||||
|
|
||||||
|
# As this is not a long running service, health-checks are not required. ClusterFuzzLite
|
||||||
|
# also only works if the user remains unchanged from the base image (it expects to run
|
||||||
|
# as root).
|
||||||
|
# trunk-ignore-all(trivy/DS026): No healthcheck is needed for this builder container
|
||||||
|
# trunk-ignore-all(checkov/CKV_DOCKER_2): No healthcheck is needed for this builder container
|
||||||
|
# trunk-ignore-all(checkov/CKV_DOCKER_3): We must run as root for this 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/
|
||||||
|
|
||||||
|
# https://docs.platformio.org/en/latest/envvars.html
|
||||||
|
ENV PLATFORMIO_CORE_DIR=$SRC/pio/core \
|
||||||
|
PLATFORMIO_LIBDEPS_DIR=$SRC/pio/libdeps \
|
||||||
|
PLATFORMIO_PACKAGES_DIR=$SRC/pio/packages \
|
||||||
|
PLATFORMIO_SETTING_ENABLE_CACHE=No \
|
||||||
|
PIO_ENV=buildroot
|
||||||
|
RUN platformio pkg install --environment $PIO_ENV
|
59
.clusterfuzzlite/README.md
Normal file
59
.clusterfuzzlite/README.md
Normal file
@ -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.
|
71
.clusterfuzzlite/build.sh
Normal file
71
.clusterfuzzlite/build.sh
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash -eu
|
||||||
|
|
||||||
|
# Build Meshtastic and a few needed dependencies using clang++
|
||||||
|
# and the OSS-Fuzz required build flags.
|
||||||
|
|
||||||
|
env
|
||||||
|
|
||||||
|
cd "$SRC"
|
||||||
|
NPROC=$(nproc || echo 1)
|
||||||
|
|
||||||
|
LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp/$SANITIZER" \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF
|
||||||
|
cmake --build "$WORK/yaml-cpp/$SANITIZER" -j "$NPROC"
|
||||||
|
cmake --install "$WORK/yaml-cpp/$SANITIZER" --prefix /usr
|
||||||
|
|
||||||
|
cmake -S "$SRC/orcania" -B "$WORK/orcania/$SANITIZER" \
|
||||||
|
-DBUILD_STATIC=ON
|
||||||
|
cmake --build "$WORK/orcania/$SANITIZER" -j "$NPROC"
|
||||||
|
cmake --install "$WORK/orcania/$SANITIZER" --prefix /usr
|
||||||
|
|
||||||
|
cmake -S "$SRC/yder" -B "$WORK/yder/$SANITIZER" \
|
||||||
|
-DBUILD_STATIC=ON -DWITH_JOURNALD=OFF
|
||||||
|
cmake --build "$WORK/yder/$SANITIZER" -j "$NPROC"
|
||||||
|
cmake --install "$WORK/yder/$SANITIZER" --prefix /usr
|
||||||
|
|
||||||
|
cmake -S "$SRC/ulfius" -B "$WORK/ulfius/$SANITIZER" \
|
||||||
|
-DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF
|
||||||
|
cmake --build "$WORK/ulfius/$SANITIZER" -j "$NPROC"
|
||||||
|
cmake --install "$WORK/ulfius/$SANITIZER" --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 PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
|
||||||
|
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"
|
||||||
|
program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program"
|
||||||
|
cp "$program" "$OUT/$fuzzer"
|
||||||
|
|
||||||
|
# Copy shared libraries used by the fuzzer.
|
||||||
|
read -d '' -ra shared_libs < <(ldd "$program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') || true
|
||||||
|
cp -f "${shared_libs[@]}" "$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
.clusterfuzzlite/platformio-clusterfuzzlite-post.py
Normal file
35
.clusterfuzzlite/platformio-clusterfuzzlite-post.py
Normal file
@ -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",
|
||||||
|
),
|
||||||
|
)
|
52
.clusterfuzzlite/platformio-clusterfuzzlite-pre.py
Normal file
52
.clusterfuzzlite/platformio-clusterfuzzlite-pre.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""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()
|
||||||
|
|
||||||
|
cxxflags = shlex.split(os.getenv("CXXFLAGS"))
|
||||||
|
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=cxxflags,
|
||||||
|
LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"],
|
||||||
|
LINKFLAGS=cxxflags
|
||||||
|
+ 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
.clusterfuzzlite/project.yaml
Normal file
1
.clusterfuzzlite/project.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
language: c++
|
206
.clusterfuzzlite/router_fuzzer.cpp
Normal file
206
.clusterfuzzlite/router_fuzzer.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage.
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <mutex>
|
||||||
|
#include <pb_decode.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "PortduinoGPIO.h"
|
||||||
|
#include "PortduinoGlue.h"
|
||||||
|
#include "PowerFSM.h"
|
||||||
|
#include "mesh/MeshTypes.h"
|
||||||
|
#include "mesh/NodeDB.h"
|
||||||
|
#include "mesh/Router.h"
|
||||||
|
#include "mesh/TypeConversions.h"
|
||||||
|
#include "mesh/mesh-pb-constants.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint32_t nodeId = 0x12345678;
|
||||||
|
// Set to true when lateInitVariant finishes. Used to ensure lateInitVariant was called during startup.
|
||||||
|
bool hasBeenConfigured = false;
|
||||||
|
|
||||||
|
// These are used to block the Arduino loop() function until a fuzzer input is ready. This is
|
||||||
|
// an optimization that prevents a sleep from happening before the loop is run. The Arduino loop
|
||||||
|
// function calls loopCanSleep() before sleeping. loopCanSleep is implemented here in the fuzzer
|
||||||
|
// and blocks until runLoopOnce() is called to signal for the loop to run.
|
||||||
|
bool fuzzerRunning = false; // Set to true once LLVMFuzzerTestOneInput has started running.
|
||||||
|
bool loopCanRun = true; // The main Arduino loop() can run when this is true.
|
||||||
|
bool loopIsWaiting = false; // The main Arduino loop() is waiting to be signaled to run.
|
||||||
|
bool loopShouldExit = false; // Indicates that the main Arduino thread should exit by throwing ShouldExitException.
|
||||||
|
std::mutex loopLock;
|
||||||
|
std::condition_variable loopCV;
|
||||||
|
std::thread meshtasticThread;
|
||||||
|
|
||||||
|
// This exception is thrown when the portuino main thread should exit.
|
||||||
|
class ShouldExitException : public std::runtime_error
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using std::runtime_error::runtime_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the loop for one test case and wait till the loop has completed. This ensures fuzz
|
||||||
|
// test cases do not overlap with one another. This helps the fuzzer attribute a crash to the
|
||||||
|
// single, currently running, test case.
|
||||||
|
void runLoopOnce()
|
||||||
|
{
|
||||||
|
realHardware = true; // Avoids delay(100) within portduino/main.cpp
|
||||||
|
std::unique_lock<std::mutex> lck(loopLock);
|
||||||
|
fuzzerRunning = true;
|
||||||
|
loopCanRun = true;
|
||||||
|
loopCV.notify_one();
|
||||||
|
loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; });
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Called in the main Arduino loop function to determine if the loop can delay/sleep before running again.
|
||||||
|
// We use this as a way to block the loop from sleeping and to start the loop function immediately when a
|
||||||
|
// fuzzer input is ready.
|
||||||
|
bool loopCanSleep()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lck(loopLock);
|
||||||
|
loopIsWaiting = true;
|
||||||
|
loopCV.notify_one();
|
||||||
|
loopCV.wait(lck, [] { return loopCanRun || loopShouldExit; });
|
||||||
|
loopIsWaiting = false;
|
||||||
|
if (loopShouldExit)
|
||||||
|
throw ShouldExitException("exit");
|
||||||
|
if (!fuzzerRunning)
|
||||||
|
return true; // The loop can sleep before the fuzzer starts.
|
||||||
|
loopCanRun = false; // Only run the loop once before waiting again.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
|
||||||
|
void lateInitVariant()
|
||||||
|
{
|
||||||
|
settingsMap[logoutputlevel] = level_error;
|
||||||
|
channelFile.channels[0] = meshtastic_Channel{
|
||||||
|
.has_settings = true,
|
||||||
|
.settings =
|
||||||
|
meshtastic_ChannelSettings{
|
||||||
|
.psk = {.size = 1, .bytes = {/*defaultpskIndex=*/1}},
|
||||||
|
.name = "LongFast",
|
||||||
|
.uplink_enabled = true,
|
||||||
|
.has_module_settings = true,
|
||||||
|
.module_settings = {.position_precision = 16},
|
||||||
|
},
|
||||||
|
.role = meshtastic_Channel_Role_PRIMARY,
|
||||||
|
};
|
||||||
|
config.security.admin_key[0] = {
|
||||||
|
.size = 32,
|
||||||
|
.bytes = {0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a,
|
||||||
|
0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c},
|
||||||
|
};
|
||||||
|
config.security.admin_key_count = 1;
|
||||||
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
|
||||||
|
moduleConfig.has_mqtt = true;
|
||||||
|
moduleConfig.mqtt = meshtastic_ModuleConfig_MQTTConfig{
|
||||||
|
.enabled = true,
|
||||||
|
.proxy_to_client_enabled = true,
|
||||||
|
};
|
||||||
|
moduleConfig.has_store_forward = true;
|
||||||
|
moduleConfig.store_forward = meshtastic_ModuleConfig_StoreForwardConfig{
|
||||||
|
.enabled = true,
|
||||||
|
.history_return_max = 4,
|
||||||
|
.history_return_window = 600,
|
||||||
|
.is_server = true,
|
||||||
|
};
|
||||||
|
meshtastic_Position fixedGPS = meshtastic_Position{
|
||||||
|
.has_latitude_i = true,
|
||||||
|
.latitude_i = static_cast<uint32_t>(1 * 1e7),
|
||||||
|
.has_longitude_i = true,
|
||||||
|
.longitude_i = static_cast<uint32_t>(3 * 1e7),
|
||||||
|
.has_altitude = true,
|
||||||
|
.altitude = 64,
|
||||||
|
.location_source = meshtastic_Position_LocSource_LOC_MANUAL,
|
||||||
|
};
|
||||||
|
nodeDB->setLocalPosition(fixedGPS);
|
||||||
|
config.has_position = true;
|
||||||
|
config.position.fixed_position = true;
|
||||||
|
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
info->has_position = true;
|
||||||
|
info->position = TypeConversions::ConvertToPositionLite(fixedGPS);
|
||||||
|
hasBeenConfigured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary.
|
||||||
|
|
||||||
|
// Start Meshtastic in a thread and wait till it has reached the ON state.
|
||||||
|
int LLVMFuzzerInitialize(int *argc, char ***argv)
|
||||||
|
{
|
||||||
|
settingsMap[maxtophone] = 5;
|
||||||
|
|
||||||
|
meshtasticThread = std::thread([program = *argv[0]]() {
|
||||||
|
char nodeIdStr[12];
|
||||||
|
strcpy(nodeIdStr, std::to_string(nodeId).c_str());
|
||||||
|
int argc = 7;
|
||||||
|
char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr};
|
||||||
|
try {
|
||||||
|
portduino_main(argc, argv);
|
||||||
|
} catch (const ShouldExitException &) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::atexit([] {
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lck(loopLock);
|
||||||
|
loopShouldExit = true;
|
||||||
|
loopCV.notify_one();
|
||||||
|
}
|
||||||
|
meshtasticThread.join();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for startup.
|
||||||
|
for (int i = 1; i < 20; ++i) {
|
||||||
|
if (powerFSM.getState() == &stateON) {
|
||||||
|
assert(hasBeenConfigured);
|
||||||
|
assert(router);
|
||||||
|
assert(nodeDB);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main entrypoint for the fuzzer (the fuzz target). The fuzzer will provide an array of bytes to be
|
||||||
|
// interpreted by this method. To keep things simple, the bytes are interpreted as a binary serialized MeshPacket
|
||||||
|
// proto. Any crashes discovered by the fuzzer will be written to a file. Unserialize that file to print the MeshPacket
|
||||||
|
// that caused the failure.
|
||||||
|
//
|
||||||
|
// This guide provides best practices for writing a fuzzer target.
|
||||||
|
// https://github.com/google/fuzzing/blob/master/docs/good-fuzz-target.md
|
||||||
|
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length)
|
||||||
|
{
|
||||||
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_default;
|
||||||
|
pb_istream_t stream = pb_istream_from_buffer(data, length);
|
||||||
|
// Ignore any inputs that fail to decode or have fields set that are not transmitted over LoRa.
|
||||||
|
if (!pb_decode(&stream, &meshtastic_MeshPacket_msg, &p) || p.rx_time || p.rx_snr || p.priority || p.rx_rssi || p.delayed ||
|
||||||
|
p.public_key.size || p.next_hop || p.relay_node || p.tx_after)
|
||||||
|
return -1; // Reject: The input will not be added to the corpus.
|
||||||
|
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||||
|
meshtastic_Data d;
|
||||||
|
stream = pb_istream_from_buffer(p.decoded.payload.bytes, p.decoded.payload.size);
|
||||||
|
if (!pb_decode(&stream, &meshtastic_Data_msg, &d))
|
||||||
|
return -1; // Reject: The input will not be added to the corpus.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide default values for a few fields so the fuzzer doesn't need to guess them.
|
||||||
|
if (p.from == 0)
|
||||||
|
p.from = nodeDB->getNodeNum();
|
||||||
|
if (p.to == 0)
|
||||||
|
p.to = nodeDB->getNodeNum();
|
||||||
|
static uint32_t packetId = 0;
|
||||||
|
if (p.id == 0)
|
||||||
|
p.id == ++packetId;
|
||||||
|
if (p.pki_encrypted && config.security.admin_key_count)
|
||||||
|
memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key));
|
||||||
|
|
||||||
|
router->enqueueReceivedMessage(packetPool.allocCopy(p));
|
||||||
|
runLoopOnce();
|
||||||
|
return 0; // Accept: The input may be added to the corpus.
|
||||||
|
}
|
||||||
|
}
|
2
.clusterfuzzlite/router_fuzzer.options
Normal file
2
.clusterfuzzlite/router_fuzzer.options
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[libfuzzer]
|
||||||
|
max_len=256
|
168
.clusterfuzzlite/router_fuzzer_seed_corpus.py
Normal file
168
.clusterfuzzlite/router_fuzzer_seed_corpus.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
"""Generate an initial set of MeshPackets.
|
||||||
|
|
||||||
|
The fuzzer uses these MeshPackets as an initial seed of test candidates.
|
||||||
|
|
||||||
|
It's also good to add any previously discovered crash test cases to this list
|
||||||
|
to avoid future regressions.
|
||||||
|
|
||||||
|
If left unset, the following values will be automatically set by the fuzzer.
|
||||||
|
- to: automatically set to the running node's NodeID
|
||||||
|
- from: automatically set to the running node's NodeID
|
||||||
|
- id: automatically set to the value of an incrementing counter
|
||||||
|
|
||||||
|
Additionally, if `pki_encrypted` is populated in the packet, the first admin key
|
||||||
|
will be copied into the `public_key` field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from meshtastic import BROADCAST_NUM
|
||||||
|
from meshtastic.protobuf import (
|
||||||
|
admin_pb2,
|
||||||
|
atak_pb2,
|
||||||
|
mesh_pb2,
|
||||||
|
portnums_pb2,
|
||||||
|
telemetry_pb2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def From(node: int = 9):
|
||||||
|
"""Return a dict suitable for **kwargs for populating the 'from' field.
|
||||||
|
|
||||||
|
'from' is a reserved keyword in Python. It can't be used directly as an
|
||||||
|
argument to the MeshPacket constructor. Rather **From() can be used as
|
||||||
|
the final argument to provide the from node as a **kwarg.
|
||||||
|
|
||||||
|
Defaults to 9 if no value is provided.
|
||||||
|
"""
|
||||||
|
return {"from": node}
|
||||||
|
|
||||||
|
|
||||||
|
packets = (
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.POSITION_APP,
|
||||||
|
payload=mesh_pb2.Position(
|
||||||
|
latitude_i=int(1 * 1e7),
|
||||||
|
longitude_i=int(2 * 1e7),
|
||||||
|
altitude=5,
|
||||||
|
precision_bits=32,
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
to=BROADCAST_NUM,
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"telemetry",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.TELEMETRY_APP,
|
||||||
|
payload=telemetry_pb2.Telemetry(
|
||||||
|
time=1736192207,
|
||||||
|
device_metrics=telemetry_pb2.DeviceMetrics(
|
||||||
|
battery_level=101,
|
||||||
|
channel_utilization=8,
|
||||||
|
air_util_tx=2,
|
||||||
|
uptime_seconds=42,
|
||||||
|
),
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
to=BROADCAST_NUM,
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"text",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
||||||
|
payload=b"Hello world",
|
||||||
|
),
|
||||||
|
to=BROADCAST_NUM,
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.NODEINFO_APP,
|
||||||
|
payload=mesh_pb2.User(
|
||||||
|
id="!00000009",
|
||||||
|
long_name="Node 9",
|
||||||
|
short_name="N9",
|
||||||
|
macaddr=b"\x00\x00\x00\x00\x00\x09",
|
||||||
|
hw_model=mesh_pb2.HardwareModel.RAK4631,
|
||||||
|
public_key=base64.b64decode(
|
||||||
|
"L0ih/6F41itofdE8mYyHk1SdfOJ/QRM1KQ+pO4vEEjQ="
|
||||||
|
),
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"traceroute",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.TRACEROUTE_APP,
|
||||||
|
payload=mesh_pb2.RouteDiscovery(
|
||||||
|
route=[10],
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"routing",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.ROUTING_APP,
|
||||||
|
payload=mesh_pb2.Routing(
|
||||||
|
error_reason=mesh_pb2.Routing.NO_RESPONSE,
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"admin",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.ADMIN_APP,
|
||||||
|
payload=admin_pb2.AdminMessage(
|
||||||
|
get_owner_request=True,
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
pki_encrypted=True,
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"atak",
|
||||||
|
mesh_pb2.MeshPacket(
|
||||||
|
decoded=mesh_pb2.Data(
|
||||||
|
portnum=portnums_pb2.PortNum.ATAK_PLUGIN,
|
||||||
|
payload=atak_pb2.TAKPacket(
|
||||||
|
is_compressed=True,
|
||||||
|
# Note, the strings are not valid for a compressed message, but will
|
||||||
|
# give the fuzzer a starting point.
|
||||||
|
contact=atak_pb2.Contact(
|
||||||
|
callsign="callsign", device_callsign="device_callsign"
|
||||||
|
),
|
||||||
|
chat=atak_pb2.GeoChat(
|
||||||
|
message="message", to="to", to_callsign="to_callsign"
|
||||||
|
),
|
||||||
|
).SerializeToString(),
|
||||||
|
),
|
||||||
|
**From(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, packet in packets:
|
||||||
|
with open(f"{name}.MeshPacket", "wb") as f:
|
||||||
|
f.write(packet.SerializeToString())
|
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
.gitignore
|
@ -20,7 +20,7 @@ extra_scripts = bin/platformio-custom.py
|
|||||||
build_flags = -Wno-missing-field-initializers
|
build_flags = -Wno-missing-field-initializers
|
||||||
|
|
||||||
-Wno-format
|
-Wno-format
|
||||||
-Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,.pio/build/output.map
|
-Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,${platformio.build_dir}/output.map
|
||||||
-DUSE_THREAD_NAMES
|
-DUSE_THREAD_NAMES
|
||||||
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||||
-DPB_ENABLE_MALLOC=1
|
-DPB_ENABLE_MALLOC=1
|
||||||
|
@ -203,7 +203,7 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
|||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
meshtastic_FileInfo fileInfo = {"", file.size()};
|
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
strcpy(fileInfo.file_name, file.path());
|
strcpy(fileInfo.file_name, file.path());
|
||||||
#else
|
#else
|
||||||
|
@ -4,7 +4,11 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
/// We clear our old flood record 10 minutes after we see the last of it
|
/// We clear our old flood record 10 minutes after we see the last of it
|
||||||
|
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
|
||||||
|
#define FLOOD_EXPIRE_TIME (5 * 1000L) // Don't allow too many packets to accumulate when fuzzing.
|
||||||
|
#else
|
||||||
#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L)
|
#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L)
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A record of a recent message broadcast
|
* A record of a recent message broadcast
|
||||||
|
@ -74,11 +74,17 @@ template <class T> class TypedQueue
|
|||||||
{
|
{
|
||||||
std::queue<T> q;
|
std::queue<T> q;
|
||||||
concurrency::OSThread *reader = NULL;
|
concurrency::OSThread *reader = NULL;
|
||||||
|
int maxElements;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TypedQueue(int maxElements) {}
|
explicit TypedQueue(int _maxElements) : maxElements(_maxElements) {}
|
||||||
|
|
||||||
int numFree() { return 1; } // Always claim 1 free, because we can grow to any size
|
int numFree()
|
||||||
|
{
|
||||||
|
if (maxElements <= 0)
|
||||||
|
return 1; // Always claim 1 free, because we can grow to any size
|
||||||
|
return maxElements - numUsed();
|
||||||
|
}
|
||||||
|
|
||||||
bool isEmpty() { return q.empty(); }
|
bool isEmpty() { return q.empty(); }
|
||||||
|
|
||||||
@ -86,6 +92,9 @@ template <class T> class TypedQueue
|
|||||||
|
|
||||||
bool enqueue(T x, TickType_t maxWait = portMAX_DELAY)
|
bool enqueue(T x, TickType_t maxWait = portMAX_DELAY)
|
||||||
{
|
{
|
||||||
|
if (numFree() <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (reader) {
|
if (reader) {
|
||||||
reader->setInterval(0);
|
reader->setInterval(0);
|
||||||
concurrency::mainDelay.interrupt();
|
concurrency::mainDelay.interrupt();
|
||||||
|
@ -21,6 +21,10 @@
|
|||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||||
|
#include <cxxabi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "platform/portduino/USBHal.h"
|
#include "platform/portduino/USBHal.h"
|
||||||
|
|
||||||
std::map<configNames, int> settingsMap;
|
std::map<configNames, int> settingsMap;
|
||||||
@ -318,8 +322,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line)
|
|||||||
gpioBind(csPin);
|
gpioBind(csPin);
|
||||||
return ERRNO_OK;
|
return ERRNO_OK;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
std::exception_ptr p = std::current_exception();
|
const std::type_info *t = abi::__cxa_current_exception_type();
|
||||||
std::cout << "Warning, cannot claim pin " << gpio_name << (p ? p.__cxa_exception_type()->name() : "null") << std::endl;
|
std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl;
|
||||||
return ERRNO_DISABLED;
|
return ERRNO_DISABLED;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
Loading…
Reference in New Issue
Block a user