From b0fe5ef8ba13af2ddc774018b9f08217859d7c3d Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 16 Jan 2025 16:42:21 -0800 Subject: [PATCH] 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 --- .clusterfuzzlite/Dockerfile | 52 +++++ .clusterfuzzlite/README.md | 59 +++++ .clusterfuzzlite/build.sh | 71 ++++++ .../platformio-clusterfuzzlite-post.py | 35 +++ .../platformio-clusterfuzzlite-pre.py | 52 +++++ .clusterfuzzlite/project.yaml | 1 + .clusterfuzzlite/router_fuzzer.cpp | 206 ++++++++++++++++++ .clusterfuzzlite/router_fuzzer.options | 2 + .clusterfuzzlite/router_fuzzer_seed_corpus.py | 168 ++++++++++++++ .dockerignore | 1 + platformio.ini | 2 +- src/FSCommon.cpp | 2 +- src/mesh/PacketHistory.h | 4 + src/mesh/TypedQueue.h | 15 +- src/platform/portduino/PortduinoGlue.cpp | 8 +- 15 files changed, 671 insertions(+), 7 deletions(-) create mode 100644 .clusterfuzzlite/Dockerfile create mode 100644 .clusterfuzzlite/README.md create mode 100644 .clusterfuzzlite/build.sh create mode 100644 .clusterfuzzlite/platformio-clusterfuzzlite-post.py create mode 100644 .clusterfuzzlite/platformio-clusterfuzzlite-pre.py create mode 100644 .clusterfuzzlite/project.yaml create mode 100644 .clusterfuzzlite/router_fuzzer.cpp create mode 100644 .clusterfuzzlite/router_fuzzer.options create mode 100644 .clusterfuzzlite/router_fuzzer_seed_corpus.py create mode 120000 .dockerignore diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 000000000..a769a976d --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -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 diff --git a/.clusterfuzzlite/README.md b/.clusterfuzzlite/README.md new file mode 100644 index 000000000..f6e4089a3 --- /dev/null +++ b/.clusterfuzzlite/README.md @@ -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_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. diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 000000000..10a2db0bd --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -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 diff --git a/.clusterfuzzlite/platformio-clusterfuzzlite-post.py b/.clusterfuzzlite/platformio-clusterfuzzlite-post.py new file mode 100644 index 000000000..f62078bb3 --- /dev/null +++ b/.clusterfuzzlite/platformio-clusterfuzzlite-post.py @@ -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", + ), +) diff --git a/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py b/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py new file mode 100644 index 000000000..a70630cf0 --- /dev/null +++ b/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py @@ -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 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", + ], +) diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 000000000..b4788012b --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c++ diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp new file mode 100644 index 000000000..bc4d248db --- /dev/null +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -0,0 +1,206 @@ +// Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage. +#include +#include +#include +#include +#include +#include +#include + +#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 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 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(1 * 1e7), + .has_longitude_i = true, + .longitude_i = static_cast(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 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. +} +} \ No newline at end of file diff --git a/.clusterfuzzlite/router_fuzzer.options b/.clusterfuzzlite/router_fuzzer.options new file mode 100644 index 000000000..7cbd646dc --- /dev/null +++ b/.clusterfuzzlite/router_fuzzer.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len=256 diff --git a/.clusterfuzzlite/router_fuzzer_seed_corpus.py b/.clusterfuzzlite/router_fuzzer_seed_corpus.py new file mode 100644 index 000000000..71736c147 --- /dev/null +++ b/.clusterfuzzlite/router_fuzzer_seed_corpus.py @@ -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()) diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 000000000..3e4e48b0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index e6735ec23..217e75631 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 6d8ff835c..1f2994b29 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -203,7 +203,7 @@ std::vector getFiles(const char *dirname, uint8_t levels) file.close(); } } else { - meshtastic_FileInfo fileInfo = {"", file.size()}; + meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; #ifdef ARCH_ESP32 strcpy(fileInfo.file_name, file.path()); #else diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 89d237a02..0417d0997 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -4,7 +4,11 @@ #include /// 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 diff --git a/src/mesh/TypedQueue.h b/src/mesh/TypedQueue.h index f7d016f10..47d7200a5 100644 --- a/src/mesh/TypedQueue.h +++ b/src/mesh/TypedQueue.h @@ -74,11 +74,17 @@ template class TypedQueue { std::queue 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 TypedQueue bool enqueue(T x, TickType_t maxWait = portMAX_DELAY) { + if (numFree() <= 0) + return false; + if (reader) { reader->setInterval(0); concurrency::mainDelay.interrupt(); @@ -112,4 +121,4 @@ template class TypedQueue void setReader(concurrency::OSThread *t) { reader = t; } }; -#endif +#endif \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index ce2418e86..5b1fe9c8c 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -21,6 +21,10 @@ #include #include +#ifdef PORTDUINO_LINUX_HARDWARE +#include +#endif + #include "platform/portduino/USBHal.h" std::map 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