From a085614aaa602ab44cb453609d3a47bb70ed0f24 Mon Sep 17 00:00:00 2001 From: danwelch3 Date: Thu, 16 Jan 2025 16:22:27 -0700 Subject: [PATCH 1/4] Initiate magnetometer based compass calibration from button presses (#5553) * Initiate magenetometer based compass calibration from button presses - only active for BMX160 accelerometers on RAK_4631 - replace automatic calibration on power on with button triggered calibration - set 5 presses to trigger 30s calibration - set 6 presses to trigger 60s calibration (useful if unit is not handheld, ie vehicle mounted) - show calibration time remaining on calibration alert screen * Fix non RAK 4631 builds - exclude changes from non RAK 4631 builds - remove calls to screen when not present * Fix build on RAK4631_eth_gw - exclude all compass heading updates on variant without screen --------- Co-authored-by: Ben Meadors --- src/ButtonThread.cpp | 14 +++++++++++++ src/graphics/Screen.h | 6 ++++++ src/motion/AccelerometerThread.h | 7 +++++++ src/motion/BMX160Sensor.cpp | 35 ++++++++++++++++++++++++++------ src/motion/BMX160Sensor.h | 1 + src/motion/MotionSensor.cpp | 8 ++++++++ src/motion/MotionSensor.h | 6 ++++++ 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 3f64b3b3e..5175a2680 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -190,6 +190,20 @@ int32_t ButtonThread::runOnce() case 4: digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); break; +#endif +#if defined(RAK_4631) + // 5 clicks: start accelerometer/magenetometer calibration for 30 seconds + case 5: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + // 6 clicks: start accelerometer/magenetometer calibration for 60 seconds + case 6: + if (accelerometerThread) { + accelerometerThread->calibrate(60); + } + break; #endif // No valid multipress action default: diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 3cb39e8ec..ce416156f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -278,6 +278,10 @@ class Screen : public concurrency::OSThread bool hasHeading() { return hasCompass; } long getHeading() { return compassHeading; } + + void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; } + uint32_t getEndCalibration() { return endCalibrationAt; } + // functions for display brightness void increaseBrightness(); void decreaseBrightness(); @@ -673,6 +677,8 @@ class Screen : public concurrency::OSThread bool hasCompass = false; float compassHeading; + uint32_t endCalibrationAt; + /// Holds state for debug information DebugInfo debugInfo; diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 95f09910f..6e517d6b0 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -48,6 +48,13 @@ class AccelerometerThread : public concurrency::OSThread setIntervalFromNow(0); }; + void calibrate(uint16_t forSeconds) + { + if (sensor) { + sensor->calibrate(forSeconds); + } + } + protected: int32_t runOnce() override { diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index 6562a651c..06cea3229 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -25,19 +25,21 @@ bool BMX160Sensor::init() int32_t BMX160Sensor::runOnce() { +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) sBmx160SensorData_t magAccel; sBmx160SensorData_t gAccel; /* Get a new sensor event */ sensor.getAllData(&magAccel, NULL, &gAccel); -#if !defined(MESHTASTIC_EXCLUDE_SCREEN) - // experimental calibrate routine. Limited to between 10 and 30 seconds after boot - if (millis() > 12 * 1000 && millis() < 30 * 1000) { + if (doCalibration) { + if (!showingScreen) { + powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; screen->startAlert((FrameCallback)drawFrameCalibration); } + if (magAccel.x > highestX) highestX = magAccel.x; if (magAccel.x < lowestX) @@ -50,9 +52,17 @@ int32_t BMX160Sensor::runOnce() highestZ = magAccel.z; if (magAccel.z < lowestZ) lowestZ = magAccel.z; - } else if (showingScreen && millis() >= 30 * 1000) { - showingScreen = false; - screen->endAlert(); + + uint32_t now = millis(); + if (now > endCalibrationAt) { + doCalibration = false; + endCalibrationAt = 0; + showingScreen = false; + screen->endAlert(); + } + + // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, + // lowestY, highestY, lowestZ, highestZ); } int highestRealX = highestX - (highestX + lowestX) / 2; @@ -93,12 +103,25 @@ int32_t BMX160Sensor::runOnce() heading += 270; break; } + screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; } +void BMX160Sensor::calibrate(uint16_t forSeconds) +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + LOG_DEBUG("BMX160 calibration started for %is", forSeconds); + + doCalibration = true; + uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided + endCalibrationAt = millis() + calibrateFor; + screen->setEndCalibration(endCalibrationAt); +#endif +} + #endif #endif \ No newline at end of file diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h index 26f477271..9031b4504 100755 --- a/src/motion/BMX160Sensor.h +++ b/src/motion/BMX160Sensor.h @@ -23,6 +23,7 @@ class BMX160Sensor : public MotionSensor explicit BMX160Sensor(ScanI2C::FoundDevice foundDevice); virtual bool init() override; virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; }; #else diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 242e3709f..d87380085 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -2,6 +2,8 @@ #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +char timeRemainingBuffer[12]; + // screen is defined in main.cpp extern graphics::Screen *screen; @@ -37,6 +39,12 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_MEDIUM); display->drawString(x, y, "Calibrating\nCompass"); + + uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000; + sprintf(timeRemainingBuffer, "( %02d )", timeRemaining); + display->setFont(FONT_SMALL); + display->drawString(x, y + 40, timeRemainingBuffer); + int16_t compassX = 0, compassY = 0; uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 78eec54ce..1f4d093bf 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -40,6 +40,8 @@ class MotionSensor // Refer to /src/concurrency/OSThread.h for more information inline virtual int32_t runOnce() { return MOTION_SENSOR_CHECK_INTERVAL_MS; }; + virtual void calibrate(uint16_t forSeconds){}; + protected: // Turn on the screen when a tap or motion is detected virtual void wakeScreen(); @@ -53,6 +55,10 @@ class MotionSensor #endif ScanI2C::FoundDevice device; + + // Do calibration if true + bool doCalibration = false; + uint32_t endCalibrationAt = 0; }; namespace MotionSensorI2C From 8179e61fdc5ed73a2deec577312cf12918efbe5c Mon Sep 17 00:00:00 2001 From: SignalMedic Date: Thu, 16 Jan 2025 18:26:02 -0500 Subject: [PATCH 2/4] changed GPS buad rate to 9600 (#5786) Co-authored-by: Huston Hedinger <1875033+hdngr@users.noreply.github.com> --- variants/canaryone/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/canaryone/variant.h b/variants/canaryone/variant.h index 140b605ad..836fa74a3 100644 --- a/variants/canaryone/variant.h +++ b/variants/canaryone/variant.h @@ -123,7 +123,7 @@ static const uint8_t A0 = PIN_A0; */ #define HAS_GPS 1 #define GPS_UBLOX -#define GPS_BAUDRATE 38400 +#define GPS_BAUDRATE 9600 // #define PIN_GPS_WAKE (GPIO_PORT1 + 2) // An output to wake GPS, low means allow sleep, high means force wake // Seems to be missing on this new board From f132158c3e83fd95004be5d19b302a6d493c295d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Hol=C3=A1sek?= Date: Fri, 17 Jan 2025 01:39:39 +0100 Subject: [PATCH 3/4] Fixed localization on bigger screens (#5695) Co-authored-by: Ben Meadors --- src/graphics/ScreenFonts.h | 102 ++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 7881b464f..f6abec0f5 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -16,63 +16,61 @@ #include "graphics/fonts/OLEDDisplayFontsCS.h" #endif +#ifdef OLED_PL +#define FONT_SMALL_LOCAL ArialMT_Plain_10_PL +#else +#ifdef OLED_RU +#define FONT_SMALL_LOCAL ArialMT_Plain_10_RU +#else +#ifdef OLED_UA +#define FONT_SMALL_LOCAL ArialMT_Plain_10_UA // Height: 13 +#else +#ifdef OLED_CS +#define FONT_SMALL_LOCAL ArialMT_Plain_10_CS +#else +#define FONT_SMALL_LOCAL ArialMT_Plain_10 // Height: 13 +#endif +#endif +#endif +#endif +#ifdef OLED_PL +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 +#else +#ifdef OLED_UA +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19 +#else +#ifdef OLED_CS +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_CS +#else +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16 // Height: 19 +#endif +#endif +#endif +#ifdef OLED_PL +#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 +#else +#ifdef OLED_UA +#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28 +#else +#ifdef OLED_CS +#define FONT_LARGE_LOCAL ArialMT_Plain_24_CS // Height: 28 +#else +#define FONT_LARGE_LOCAL ArialMT_Plain_24 // Height: 28 +#endif +#endif +#endif + #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts -#ifdef OLED_PL -#define FONT_SMALL ArialMT_Plain_16_PL // Height: 19 -#define FONT_MEDIUM ArialMT_Plain_24_PL // Height: 28 -#define FONT_LARGE ArialMT_Plain_24_PL // Height: 28 +#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 +#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 +#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #else -#define FONT_SMALL ArialMT_Plain_16 // Height: 19 -#define FONT_MEDIUM ArialMT_Plain_24 // Height: 28 -#define FONT_LARGE ArialMT_Plain_24 // Height: 28 -#endif -#else -#ifdef OLED_PL -#define FONT_SMALL ArialMT_Plain_10_PL -#else -#ifdef OLED_RU -#define FONT_SMALL ArialMT_Plain_10_RU -#else -#ifdef OLED_UA -#define FONT_SMALL ArialMT_Plain_10_UA // Height: 13 -#else -#ifdef OLED_CS -#define FONT_SMALL ArialMT_Plain_10_CS -#else -#define FONT_SMALL ArialMT_Plain_10 // Height: 13 -#endif -#endif -#endif -#endif -#ifdef OLED_PL -#define FONT_MEDIUM ArialMT_Plain_16_PL // Height: 19 -#else -#ifdef OLED_UA -#define FONT_MEDIUM ArialMT_Plain_16_UA // Height: 19 -#else -#ifdef OLED_CS -#define FONT_MEDIUM ArialMT_Plain_16_CS -#else -#define FONT_MEDIUM ArialMT_Plain_16 // Height: 19 -#endif -#endif -#endif -#ifdef OLED_PL -#define FONT_LARGE ArialMT_Plain_24_PL // Height: 28 -#else -#ifdef OLED_UA -#define FONT_LARGE ArialMT_Plain_24_UA // Height: 28 -#else -#ifdef OLED_CS -#define FONT_LARGE ArialMT_Plain_24_CS // Height: 28 -#else -#define FONT_LARGE ArialMT_Plain_24 // Height: 28 -#endif -#endif -#endif +#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 +#define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19 +#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif #define _fontHeight(font) ((font)[1] + 1) // height is position 1 From b0fe5ef8ba13af2ddc774018b9f08217859d7c3d Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 16 Jan 2025 16:42:21 -0800 Subject: [PATCH 4/4] 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