From 0aeb1e8d8c6f28b79118e927ceb1cf8023e07b38 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 7 Jan 2025 23:03:01 -0800 Subject: [PATCH 01/17] Initial commit of a fuzzer for Meshtastic. --- .clusterfuzzlite/Dockerfile | 39 +++++ .clusterfuzzlite/README.md | 59 +++++++ .clusterfuzzlite/build.sh | 67 ++++++++ .../platformio-clusterfuzzlite-post.py | 35 +++++ .../platformio-clusterfuzzlite-pre.py | 48 ++++++ .clusterfuzzlite/project.yaml | 1 + .clusterfuzzlite/router_fuzzer.cpp | 144 ++++++++++++++++++ .clusterfuzzlite/router_fuzzer.options | 2 + .clusterfuzzlite/router_fuzzer_seed_corpus.py | 142 +++++++++++++++++ .dockerignore | 1 + src/FSCommon.cpp | 2 +- src/mesh/PacketHistory.h | 4 + src/mesh/Router.cpp | 4 + src/mesh/Router.h | 10 ++ src/platform/portduino/PortduinoGlue.cpp | 8 +- 15 files changed, 563 insertions(+), 3 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 0000000000..7116d4f649 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,39 @@ +# trunk-ignore-all(trivy/DS026): No healthcheck is needed for this builder container +# trunk-ignore-all(trivy/DS002): We must run as root for this container +# trunk-ignore-all(checkov/CKV_DOCKER_8): We must run as root for this container +# trunk-ignore-all(hadolint/DL3002): We must run as root for this container + +FROM gcr.io/oss-fuzz-base/base-builder:v1 + +ENV PIP_ROOT_USER_ACTION=ignore + +# trunk-ignore(hadolint/DL3008): apt packages are not pinned. +# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned. +RUN apt-get update && apt-get install --no-install-recommends -y \ + cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ + libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ + libusb-1.0-0-dev libssl-dev pkg-config && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + pip install --no-cache-dir -U \ + platformio==6.1.16 \ + grpcio-tools==1.68.1 \ + meshtastic==2.5.9 + +# Ugly hack to avoid clang detecting a conflict between the math "log" function and the "log" function in framework-portduino/cores/portduino/logging.h +RUN sed -i -e 's/__MATHCALL_VEC (log,, (_Mdouble_ __x));//' /usr/include/x86_64-linux-gnu/bits/mathcalls.h + +# A few dependencies are too old on the base-builder image. More recent versions are built from source. +WORKDIR $SRC +RUN git config --global advice.detachedHead false && \ + git clone --depth 1 --branch 0.8.0 https://github.com/jbeder/yaml-cpp.git && \ + git clone --depth 1 --branch v2.3.3 https://github.com/babelouest/orcania.git && \ + git clone --depth 1 --branch v1.4.20 https://github.com/babelouest/yder.git && \ + git clone --depth 1 --branch v2.7.15 https://github.com/babelouest/ulfius.git + +COPY ./.clusterfuzzlite/build.sh $SRC/ + +WORKDIR $SRC/firmware +COPY . $SRC/firmware/ + +ENV PIO_ENV=buildroot +RUN platformio pkg install --environment $PIO_ENV diff --git a/.clusterfuzzlite/README.md b/.clusterfuzzlite/README.md new file mode 100644 index 0000000000..6ca7ec0eb7 --- /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 is 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 0000000000..5249a365fe --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,67 @@ +#!/bin/bash -eu + +# Build Mestastic and a few needed dependencies using clang++ +# and the OSS-Fuzz required build flags. + +env + +cd "$SRC" + +LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$SRC/yaml-cpp/build" \ + -DBUILD_SHARED_LIBS=OFF +cmake --build "$SRC/yaml-cpp/build" -j "$(nproc)" +cmake --install "$SRC/yaml-cpp/build" --prefix /usr + +cmake -S "$SRC/orcania" -B "$SRC/orcania/build" \ + -DBUILD_STATIC=ON +cmake --build "$SRC/orcania/build" -j "$(nproc)" +cmake --install "$SRC/orcania/build" --prefix /usr + +cmake -S "$SRC/yder" -B "$SRC/yder/build" \ + -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF +cmake --build "$SRC/yder/build" -j "$(nproc)" +cmake --install "$SRC/yder/build" --prefix /usr + +cmake -S "$SRC/ulfius" -B "$SRC/ulfius/build" \ + -DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF +cmake --build "$SRC/ulfius/build" -j "$(nproc)" +cmake --install "$SRC/ulfius/build" --prefix /usr + +cd "$SRC/firmware" + +PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py") +STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors) +export PLATFORMIO_EXTRA_SCRIPTS +export STATIC_LIBS +export TARGET_CC=$CC +export TARGET_CXX=$CXX +export TARGET_LD=$CXX +export TARGET_AR=llvm-ar +export TARGET_AS=llvm-as +export TARGET_OBJCOPY=llvm-objcopy +export TARGET_RANLIB=llvm-ranlib + +mkdir -p "$OUT/lib" + +cp .clusterfuzzlite/*_fuzzer.options "$OUT/" + +for f in .clusterfuzzlite/*_fuzzer.cpp; do + fuzzer=$(basename "$f" .cpp) + cp -f "$f" src/fuzzer.cpp + pio run -vvv --environment "$PIO_ENV" + cp ".pio/build/$PIO_ENV/program" "$OUT/$fuzzer" + + # Copy shared libraries used by the fuzzer. + cp -f $(ldd .pio/build/buildroot/program | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') "$OUT/lib/" + + # Build the initial fuzzer seed corpus. + corpus_name="${fuzzer}_seed_corpus" + corpus_generator="$PWD/.clusterfuzzlite/${corpus_name}.py" + if [[ -f $corpus_generator ]]; then + mkdir "$corpus_name" + pushd "$corpus_name" + python3 "$corpus_generator" + popd + zip -D "$OUT/${corpus_name}.zip" "$corpus_name"/* + fi +done diff --git a/.clusterfuzzlite/platformio-clusterfuzzlite-post.py b/.clusterfuzzlite/platformio-clusterfuzzlite-post.py new file mode 100644 index 0000000000..f62078bb3b --- /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 0000000000..26a1b0f623 --- /dev/null +++ b/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py @@ -0,0 +1,48 @@ +"""PlatformIO build script (pre: runs before other Meshtastic scripts). + +ClusterFuzzLite executes in a different container from the build. During the build, +attempt to link statically to as many dependencies as possible. For dependencies that +do not have static libraries, the shared library files are copied to the output +directory by the build.sh script. +""" + +import glob +import os +import shlex + +from SCons.Script import DefaultEnvironment, Literal + +env = DefaultEnvironment() + +sanitizer_flags = shlex.split(os.getenv("SANITIZER_FLAGS")) +lib_fuzzing_engine = shlex.split(os.getenv("LIB_FUZZING_ENGINE")) +statics = glob.glob("/usr/lib/lib*.a") + glob.glob("/usr/lib/*/lib*.a") +no_static = set(("-ldl",)) + + +def replaceStatic(lib): + """Replace -l with the static .a file for the library.""" + if not lib.startswith("-l") or lib in no_static: + return lib + static_name = f"/lib{lib[2:]}.a" + static = [s for s in statics if s.endswith(static_name)] + if len(static) == 1: + return static[0] + return lib + + +# Setup the environment for building with Clang and the OSS-Fuzz required build flags. +env.Append( + CFLAGS=os.getenv("CFLAGS"), + CXXFLAGS=os.getenv("CXXFLAGS"), + LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"], + LINKFLAGS=sanitizer_flags + lib_fuzzing_engine + ["-stdlib=libc++", "-std=c++17"], + _LIBFLAGS=[replaceStatic(s) for s in shlex.split(os.getenv("STATIC_LIBS"))] + + [ + "/usr/lib/x86_64-linux-gnu/libunistring.a", # Needs to be at the end. + # Find the shared libraries in a subdirectory named lib + # within the same directory as the binary. + Literal("-Wl,-rpath,$ORIGIN/lib"), + "-Wl,-z,origin", + ], +) diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000000..b4788012b1 --- /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 0000000000..a7cd895a32 --- /dev/null +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -0,0 +1,144 @@ +// Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage. +#include +#include +#include +#include + +#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; +bool hasBeenConfigured = false; +} // namespace + +// 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) +{ + std::thread t([program = *argv[0]]() { + char nodeIdStr[12]; + strcpy(nodeIdStr, std::to_string(nodeId).c_str()); + int argc = 3; + char *argv[] = {program, "-h", nodeIdStr, nullptr}; + portduino_main(argc, argv); + }); + t.detach(); + + // 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)); + + // Ideally only one packet, the one generated by the fuzzer, is being processed by the firmware at + // a time. We acquire a lock here, and the router unlocks it after it has processed all queued packets. + // Grabbing the lock again, below, should block until the queue has been emptied. + router->inProgressLock.lock(); + router->enqueueReceivedMessage(packetPool.allocCopy(p)); + + const std::lock_guard lck(router->inProgressLock); + 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 0000000000..7cbd646dcd --- /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 0000000000..684293a627 --- /dev/null +++ b/.clusterfuzzlite/router_fuzzer_seed_corpus.py @@ -0,0 +1,142 @@ +"""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, 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(), + ), + ), +) + +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 0000000000..3e4e48b0b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 6d8ff835c6..1f2994b298 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 89d237a027..0417d09973 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/Router.cpp b/src/mesh/Router.cpp index bfd4c45fd0..b344feed15 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -70,6 +70,10 @@ int32_t Router::runOnce() perhapsHandleReceived(mp); } +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + inProgressLock.unlock(); + return 5; +#endif // LOG_DEBUG("Sleep forever!"); return INT32_MAX; // Wait a long time - until we get woken for the message queue } diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 0fe2bc5510..fa7119fe87 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -8,6 +8,10 @@ #include "RadioInterface.h" #include "concurrency/OSThread.h" +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +#include +#endif + /** * A mesh aware router that supports multiple interfaces. */ @@ -86,6 +90,12 @@ class Router : protected concurrency::OSThread before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + // Used by router_fuzzer.cpp to detect when the Router has finished processing a packet. + // See LLVMFuzzerTestOneInput in router_fuzzer.cpp & Router::runOnce for how this is used. + std::mutex inProgressLock; +#endif + protected: friend class RoutingModule; diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index b042510f50..e6f85447b0 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; @@ -343,8 +347,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName) 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 From e2afd5f2ba11e1d134cc5d9b22d7c959769ffba5 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 09:03:15 -0800 Subject: [PATCH 02/17] Use a max of 5 for the phone queues --- .clusterfuzzlite/router_fuzzer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index a7cd895a32..4dd6a0a375 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -77,6 +77,8 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht // Start Meshtastic in a thread and wait till it has reached the ON state. int LLVMFuzzerInitialize(int *argc, char ***argv) { + settingsMap[maxtophone] = 5; + std::thread t([program = *argv[0]]() { char nodeIdStr[12]; strcpy(nodeIdStr, std::to_string(nodeId).c_str()); From cf0e59d50ddc9b307467cc9122108d043a9cddbc Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 09:16:19 -0800 Subject: [PATCH 03/17] Only write files to the temp dir --- .clusterfuzzlite/router_fuzzer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index 4dd6a0a375..b28211a2f2 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -82,8 +82,8 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) std::thread t([program = *argv[0]]() { char nodeIdStr[12]; strcpy(nodeIdStr, std::to_string(nodeId).c_str()); - int argc = 3; - char *argv[] = {program, "-h", nodeIdStr, nullptr}; + int argc = 5; + char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, nullptr}; portduino_main(argc, argv); }); t.detach(); From 01417f6fd18263adfcc1172ba2caf1dbbecce8d7 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 09:46:25 -0800 Subject: [PATCH 04/17] Limitless queue + fuzzer = lots of ram :) --- src/mesh/TypedQueue.h | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/mesh/TypedQueue.h b/src/mesh/TypedQueue.h index f7d016f10f..47d7200a53 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 From 47871c6878180c1604fe2ad5fbd43b3840a527e7 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 11:25:34 -0800 Subject: [PATCH 05/17] Use $PIO_ENV for path to program --- .clusterfuzzlite/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 5249a365fe..1491646a43 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -52,7 +52,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do cp ".pio/build/$PIO_ENV/program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. - cp -f $(ldd .pio/build/buildroot/program | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') "$OUT/lib/" + cp -f $(ldd ".pio/build/$PIO_ENV/program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') "$OUT/lib/" # Build the initial fuzzer seed corpus. corpus_name="${fuzzer}_seed_corpus" From 08c82b53b738cf8cd04a6ff8df3cb316c3b4822a Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 13:18:43 -0800 Subject: [PATCH 06/17] spelling: s/is/to/ --- .clusterfuzzlite/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clusterfuzzlite/README.md b/.clusterfuzzlite/README.md index 6ca7ec0eb7..f6e4089a37 100644 --- a/.clusterfuzzlite/README.md +++ b/.clusterfuzzlite/README.md @@ -31,7 +31,7 @@ More background on these commands can be found in the ## 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 is a MeshPacket using nanopb. A few fields in +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. From 091b174e2b222447275ef980e89ebedbb04c8192 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 8 Jan 2025 17:06:42 -0800 Subject: [PATCH 07/17] Use loopCanSleep instead of a lock in Router --- .clusterfuzzlite/router_fuzzer.cpp | 47 ++++++++++++++++++++++++++---- src/mesh/Router.cpp | 4 --- src/mesh/Router.h | 10 ------- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index b28211a2f2..94711be759 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -1,4 +1,5 @@ // Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage. +#include #include #include #include @@ -15,7 +16,30 @@ 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 startLoop() 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. +std::mutex loopLock; +std::condition_variable loopCV; + +// 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() +{ + std::unique_lock lck(loopLock); + fuzzerRunning = true; + loopCanRun = true; + loopCV.notify_one(); + loopCV.wait(lck, [] { return !loopCanRun && loopIsWaiting; }); +} } // namespace // Called just prior to starting Meshtastic. Allows for setting config values before startup. @@ -71,6 +95,22 @@ void lateInitVariant() hasBeenConfigured = true; } +// 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; }); + loopIsWaiting = false; + if (!fuzzerRunning) + return true; // The loop can sleep before the fuzzer starts. + loopCanRun = false; // Only run the loop once before waiting again. + return false; +} + extern "C" { int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary. @@ -134,13 +174,8 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t length) if (p.pki_encrypted && config.security.admin_key_count) memcpy(&p.public_key, &config.security.admin_key[0], sizeof(p.public_key)); - // Ideally only one packet, the one generated by the fuzzer, is being processed by the firmware at - // a time. We acquire a lock here, and the router unlocks it after it has processed all queued packets. - // Grabbing the lock again, below, should block until the queue has been emptied. - router->inProgressLock.lock(); router->enqueueReceivedMessage(packetPool.allocCopy(p)); - - const std::lock_guard lck(router->inProgressLock); + runLoopOnce(); return 0; // Accept: The input may be added to the corpus. } } \ No newline at end of file diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index b344feed15..bfd4c45fd0 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -70,10 +70,6 @@ int32_t Router::runOnce() perhapsHandleReceived(mp); } -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION - inProgressLock.unlock(); - return 5; -#endif // LOG_DEBUG("Sleep forever!"); return INT32_MAX; // Wait a long time - until we get woken for the message queue } diff --git a/src/mesh/Router.h b/src/mesh/Router.h index fa7119fe87..0fe2bc5510 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -8,10 +8,6 @@ #include "RadioInterface.h" #include "concurrency/OSThread.h" -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -#include -#endif - /** * A mesh aware router that supports multiple interfaces. */ @@ -90,12 +86,6 @@ class Router : protected concurrency::OSThread before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION - // Used by router_fuzzer.cpp to detect when the Router has finished processing a packet. - // See LLVMFuzzerTestOneInput in router_fuzzer.cpp & Router::runOnce for how this is used. - std::mutex inProgressLock; -#endif - protected: friend class RoutingModule; From a56401306f914ad5d1f05c8420f8abbb8b517256 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 9 Jan 2025 08:27:26 -0800 Subject: [PATCH 08/17] realHardware allows full use of a CPU core --- .clusterfuzzlite/router_fuzzer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index 94711be759..1ad8e2cb2d 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -5,6 +5,7 @@ #include #include +#include "PortduinoGPIO.h" #include "PortduinoGlue.h" #include "PowerFSM.h" #include "mesh/MeshTypes.h" @@ -34,6 +35,7 @@ std::condition_variable loopCV; // 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; From cd18e5ceb4a3254aa36fe8c1209226d55014da6f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 9 Jan 2025 08:41:26 -0800 Subject: [PATCH 09/17] Ignore checkov CKV_DOCKER_2 & CKV_DOCKER_3 --- .clusterfuzzlite/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 7116d4f649..2fe909a9b6 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -1,4 +1,12 @@ +# 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 690b6d810571e6bd0fa6c0c7ced6b0f729e34b5f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 9 Jan 2025 15:43:46 -0800 Subject: [PATCH 10/17] Add Atak seed --- .clusterfuzzlite/router_fuzzer_seed_corpus.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.clusterfuzzlite/router_fuzzer_seed_corpus.py b/.clusterfuzzlite/router_fuzzer_seed_corpus.py index 684293a627..71736c1472 100644 --- a/.clusterfuzzlite/router_fuzzer_seed_corpus.py +++ b/.clusterfuzzlite/router_fuzzer_seed_corpus.py @@ -17,7 +17,13 @@ import base64 from meshtastic import BROADCAST_NUM -from meshtastic.protobuf import admin_pb2, mesh_pb2, portnums_pb2, telemetry_pb2 +from meshtastic.protobuf import ( + admin_pb2, + atak_pb2, + mesh_pb2, + portnums_pb2, + telemetry_pb2, +) def From(node: int = 9): @@ -135,6 +141,26 @@ def From(node: int = 9): **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: From 5fbe02999c0571b988924241fde685ef99527cfa Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 9 Jan 2025 23:51:13 -0800 Subject: [PATCH 11/17] Fix lint issues in build.sh --- .clusterfuzzlite/build.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 1491646a43..0f2428e768 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -1,30 +1,31 @@ #!/bin/bash -eu -# Build Mestastic and a few needed dependencies using clang++ +# 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 "$SRC/yaml-cpp/build" \ -DBUILD_SHARED_LIBS=OFF -cmake --build "$SRC/yaml-cpp/build" -j "$(nproc)" +cmake --build "$SRC/yaml-cpp/build" -j "$NPROC" cmake --install "$SRC/yaml-cpp/build" --prefix /usr cmake -S "$SRC/orcania" -B "$SRC/orcania/build" \ -DBUILD_STATIC=ON -cmake --build "$SRC/orcania/build" -j "$(nproc)" +cmake --build "$SRC/orcania/build" -j "$NPROC" cmake --install "$SRC/orcania/build" --prefix /usr cmake -S "$SRC/yder" -B "$SRC/yder/build" \ -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF -cmake --build "$SRC/yder/build" -j "$(nproc)" +cmake --build "$SRC/yder/build" -j "$NPROC" cmake --install "$SRC/yder/build" --prefix /usr cmake -S "$SRC/ulfius" -B "$SRC/ulfius/build" \ -DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF -cmake --build "$SRC/ulfius/build" -j "$(nproc)" +cmake --build "$SRC/ulfius/build" -j "$NPROC" cmake --install "$SRC/ulfius/build" --prefix /usr cd "$SRC/firmware" @@ -52,7 +53,8 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do cp ".pio/build/$PIO_ENV/program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. - cp -f $(ldd ".pio/build/$PIO_ENV/program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') "$OUT/lib/" + read -ra shared_libs < <(ldd ".pio/build/$PIO_ENV/program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') + cp -f "${shared_libs[@]}" "$OUT/lib/" # Build the initial fuzzer seed corpus. corpus_name="${fuzzer}_seed_corpus" From 31706ae15e31e54f39227187065c35c87875da3a Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 9 Jan 2025 23:53:09 -0800 Subject: [PATCH 12/17] Use exception to exit from portduino_main --- .clusterfuzzlite/router_fuzzer.cpp | 69 ++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index 1ad8e2cb2d..d5f248bfb9 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -1,7 +1,9 @@ // Fuzzer implementation that sends MeshPackets to Router::enqueueReceivedMessage. #include +#include #include #include +#include #include #include @@ -23,12 +25,21 @@ 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 startLoop() 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. +// 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 @@ -44,6 +55,24 @@ void runLoopOnce() } } // 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() { @@ -97,22 +126,6 @@ void lateInitVariant() hasBeenConfigured = true; } -// 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; }); - loopIsWaiting = false; - if (!fuzzerRunning) - return true; // The loop can sleep before the fuzzer starts. - loopCanRun = false; // Only run the loop once before waiting again. - return false; -} - extern "C" { int portduino_main(int argc, char **argv); // Renamed "main" function from Meshtastic binary. @@ -121,14 +134,24 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) { settingsMap[maxtophone] = 5; - std::thread t([program = *argv[0]]() { + meshtasticThread = std::thread([program = *argv[0]]() { char nodeIdStr[12]; strcpy(nodeIdStr, std::to_string(nodeId).c_str()); int argc = 5; char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, nullptr}; - portduino_main(argc, argv); + try { + portduino_main(argc, argv); + } catch (const ShouldExitException &) { + } + }); + std::atexit([] { + { + const std::lock_guard lck(loopLock); + loopShouldExit = true; + loopCV.notify_one(); + } + meshtasticThread.join(); }); - t.detach(); // Wait for startup. for (int i = 1; i < 20; ++i) { From 005166d95613af1328216b313108437719247d44 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 10 Jan 2025 15:36:32 -0800 Subject: [PATCH 13/17] Separate build & source files into $WORK & $SRC --- .clusterfuzzlite/Dockerfile | 8 +++++++- .clusterfuzzlite/build.sh | 29 +++++++++++++++-------------- platformio.ini | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 2fe909a9b6..8b71360055 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -43,5 +43,11 @@ COPY ./.clusterfuzzlite/build.sh $SRC/ WORKDIR $SRC/firmware COPY . $SRC/firmware/ -ENV PIO_ENV=buildroot +# https://docs.platformio.org/en/latest/envvars.html +ENV PLATFORMIO_WORKSPACE_DIR=$WORK/pio \ + 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/build.sh b/.clusterfuzzlite/build.sh index 0f2428e768..781ad9ea67 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -8,25 +8,25 @@ env cd "$SRC" NPROC=$(nproc || echo 1) -LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$SRC/yaml-cpp/build" \ +LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp" \ -DBUILD_SHARED_LIBS=OFF -cmake --build "$SRC/yaml-cpp/build" -j "$NPROC" -cmake --install "$SRC/yaml-cpp/build" --prefix /usr +cmake --build "$WORK/yaml-cpp" -j "$NPROC" +cmake --install "$WORK/yaml-cpp" --prefix /usr -cmake -S "$SRC/orcania" -B "$SRC/orcania/build" \ +cmake -S "$SRC/orcania" -B "$WORK/orcania" \ -DBUILD_STATIC=ON -cmake --build "$SRC/orcania/build" -j "$NPROC" -cmake --install "$SRC/orcania/build" --prefix /usr +cmake --build "$WORK/orcania" -j "$NPROC" +cmake --install "$WORK/orcania" --prefix /usr -cmake -S "$SRC/yder" -B "$SRC/yder/build" \ +cmake -S "$SRC/yder" -B "$WORK/yder" \ -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF -cmake --build "$SRC/yder/build" -j "$NPROC" -cmake --install "$SRC/yder/build" --prefix /usr +cmake --build "$WORK/yder" -j "$NPROC" +cmake --install "$WORK/yder" --prefix /usr -cmake -S "$SRC/ulfius" -B "$SRC/ulfius/build" \ +cmake -S "$SRC/ulfius" -B "$WORK/ulfius" \ -DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF -cmake --build "$SRC/ulfius/build" -j "$NPROC" -cmake --install "$SRC/ulfius/build" --prefix /usr +cmake --build "$WORK/ulfius" -j "$NPROC" +cmake --install "$WORK/ulfius" --prefix /usr cd "$SRC/firmware" @@ -50,10 +50,11 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do fuzzer=$(basename "$f" .cpp) cp -f "$f" src/fuzzer.cpp pio run -vvv --environment "$PIO_ENV" - cp ".pio/build/$PIO_ENV/program" "$OUT/$fuzzer" + program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program" + cp "$program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. - read -ra shared_libs < <(ldd ".pio/build/$PIO_ENV/program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') + read -ra shared_libs < <(ldd "$program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') cp -f "${shared_libs[@]}" "$OUT/lib/" # Build the initial fuzzer seed corpus. diff --git a/platformio.ini b/platformio.ini index 6a4466c016..259b2ec8c7 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 From bd32ecbc7154fc40958600044b037ae370a3a08b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 10 Jan 2025 15:38:14 -0800 Subject: [PATCH 14/17] Use an ephemeral port for the API server --- .clusterfuzzlite/router_fuzzer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index d5f248bfb9..bc4d248db9 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -137,8 +137,8 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) meshtasticThread = std::thread([program = *argv[0]]() { char nodeIdStr[12]; strcpy(nodeIdStr, std::to_string(nodeId).c_str()); - int argc = 5; - char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, nullptr}; + int argc = 7; + char *argv[] = {program, "-d", "/tmp/meshtastic", "-h", nodeIdStr, "-p", "0", nullptr}; try { portduino_main(argc, argv); } catch (const ShouldExitException &) { From 132536d4072729ce238d8a684f8945f190ab0b32 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 10 Jan 2025 15:39:25 -0800 Subject: [PATCH 15/17] Include CXXFLAGS in the link step --- .clusterfuzzlite/platformio-clusterfuzzlite-pre.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py b/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py index 26a1b0f623..a70630cf0b 100644 --- a/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py +++ b/.clusterfuzzlite/platformio-clusterfuzzlite-pre.py @@ -14,6 +14,7 @@ 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") @@ -34,9 +35,12 @@ def replaceStatic(lib): # Setup the environment for building with Clang and the OSS-Fuzz required build flags. env.Append( CFLAGS=os.getenv("CFLAGS"), - CXXFLAGS=os.getenv("CXXFLAGS"), + CXXFLAGS=cxxflags, LIBSOURCE_DIRS=["/usr/lib/x86_64-linux-gnu"], - LINKFLAGS=sanitizer_flags + lib_fuzzing_engine + ["-stdlib=libc++", "-std=c++17"], + 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. From 2b5ea69999376b323e8146677f8dcbb9d7f09d1b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 10 Jan 2025 16:16:22 -0800 Subject: [PATCH 16/17] Read all shared libraries --- .clusterfuzzlite/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 781ad9ea67..a0c5804c5d 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -54,7 +54,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do cp "$program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. - read -ra shared_libs < <(ldd "$program" | sed -n 's/[^=]\+=> \([^ ]\+\).*/\1/p') + 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. From f7f56024b39adee97fce6402f09867b783dbe267 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 11 Jan 2025 10:03:15 -0800 Subject: [PATCH 17/17] Use a separate work directory for each sanitizer --- .clusterfuzzlite/Dockerfile | 3 +-- .clusterfuzzlite/build.sh | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 8b71360055..a769a976d7 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -44,8 +44,7 @@ WORKDIR $SRC/firmware COPY . $SRC/firmware/ # https://docs.platformio.org/en/latest/envvars.html -ENV PLATFORMIO_WORKSPACE_DIR=$WORK/pio \ - PLATFORMIO_CORE_DIR=$SRC/pio/core \ +ENV PLATFORMIO_CORE_DIR=$SRC/pio/core \ PLATFORMIO_LIBDEPS_DIR=$SRC/pio/libdeps \ PLATFORMIO_PACKAGES_DIR=$SRC/pio/packages \ PLATFORMIO_SETTING_ENABLE_CACHE=No \ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index a0c5804c5d..10a2db0bd5 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -8,25 +8,25 @@ env cd "$SRC" NPROC=$(nproc || echo 1) -LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp" \ +LDFLAGS=-lpthread cmake -S "$SRC/yaml-cpp" -B "$WORK/yaml-cpp/$SANITIZER" \ -DBUILD_SHARED_LIBS=OFF -cmake --build "$WORK/yaml-cpp" -j "$NPROC" -cmake --install "$WORK/yaml-cpp" --prefix /usr +cmake --build "$WORK/yaml-cpp/$SANITIZER" -j "$NPROC" +cmake --install "$WORK/yaml-cpp/$SANITIZER" --prefix /usr -cmake -S "$SRC/orcania" -B "$WORK/orcania" \ +cmake -S "$SRC/orcania" -B "$WORK/orcania/$SANITIZER" \ -DBUILD_STATIC=ON -cmake --build "$WORK/orcania" -j "$NPROC" -cmake --install "$WORK/orcania" --prefix /usr +cmake --build "$WORK/orcania/$SANITIZER" -j "$NPROC" +cmake --install "$WORK/orcania/$SANITIZER" --prefix /usr -cmake -S "$SRC/yder" -B "$WORK/yder" \ +cmake -S "$SRC/yder" -B "$WORK/yder/$SANITIZER" \ -DBUILD_STATIC=ON -DWITH_JOURNALD=OFF -cmake --build "$WORK/yder" -j "$NPROC" -cmake --install "$WORK/yder" --prefix /usr +cmake --build "$WORK/yder/$SANITIZER" -j "$NPROC" +cmake --install "$WORK/yder/$SANITIZER" --prefix /usr -cmake -S "$SRC/ulfius" -B "$WORK/ulfius" \ +cmake -S "$SRC/ulfius" -B "$WORK/ulfius/$SANITIZER" \ -DBUILD_STATIC=ON -DWITH_JANSSON=OFF -DWITH_CURL=OFF -DWITH_WEBSOCKET=OFF -cmake --build "$WORK/ulfius" -j "$NPROC" -cmake --install "$WORK/ulfius" --prefix /usr +cmake --build "$WORK/ulfius/$SANITIZER" -j "$NPROC" +cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr cd "$SRC/firmware" @@ -34,6 +34,7 @@ PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzl 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