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
|
||||
|
||||
-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
|
||||
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||
-DPB_ENABLE_MALLOC=1
|
||||
|
@ -203,7 +203,7 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
meshtastic_FileInfo fileInfo = {"", file.size()};
|
||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||
#ifdef ARCH_ESP32
|
||||
strcpy(fileInfo.file_name, file.path());
|
||||
#else
|
||||
|
@ -4,7 +4,11 @@
|
||||
#include <unordered_set>
|
||||
|
||||
/// 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)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A record of a recent message broadcast
|
||||
|
@ -74,11 +74,17 @@ template <class T> class TypedQueue
|
||||
{
|
||||
std::queue<T> q;
|
||||
concurrency::OSThread *reader = NULL;
|
||||
int maxElements;
|
||||
|
||||
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(); }
|
||||
|
||||
@ -86,6 +92,9 @@ template <class T> class TypedQueue
|
||||
|
||||
bool enqueue(T x, TickType_t maxWait = portMAX_DELAY)
|
||||
{
|
||||
if (numFree() <= 0)
|
||||
return false;
|
||||
|
||||
if (reader) {
|
||||
reader->setInterval(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
|
@ -21,6 +21,10 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||
#include <cxxabi.h>
|
||||
#endif
|
||||
|
||||
#include "platform/portduino/USBHal.h"
|
||||
|
||||
std::map<configNames, int> settingsMap;
|
||||
@ -318,8 +322,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line)
|
||||
gpioBind(csPin);
|
||||
return ERRNO_OK;
|
||||
} catch (...) {
|
||||
std::exception_ptr p = std::current_exception();
|
||||
std::cout << "Warning, cannot claim pin " << gpio_name << (p ? p.__cxa_exception_type()->name() : "null") << std::endl;
|
||||
const std::type_info *t = abi::__cxa_current_exception_type();
|
||||
std::cout << "Warning, cannot claim pin " << gpio_name << (t ? t->name() : "null") << std::endl;
|
||||
return ERRNO_DISABLED;
|
||||
}
|
||||
#else
|
||||
|
Loading…
Reference in New Issue
Block a user