Harden against crafted-packet crashes + adversarial fuzzing (#10862)

Audit and fuzzing of the RF-packet decode -> dispatch -> display/phone paths for
the "crash a node or phone with a crafted packet" surface, beyond the XEdDSA
authenticity work.

Crash fixes (reproduced under AddressSanitizer / UBSan):
- GeoCoord::latLongToUTM/latLongToMGRS read fixed letter tables out of bounds on
  extreme latitude_i/longitude_i from a received Position, and narrowed
  out-of-range easting/northing doubles to unsigned (float-cast-overflow UB).
  Clamp the UTM zone, the easting/northing narrowing, and the band/col/row
  indices. Regression: test_geocoord_extreme_coords_no_oob.
- EnvironmentTelemetry/AirQualityTelemetry render attacker floats via
  String(float), which on nRF52/RP2040/STM32/portduino formats into a fixed
  char[33] (dtostrf) and overflows near FLT_MAX. Clamp the rendered metrics via
  UnitConversions::displaySafeFloat (finite + magnitude <= 1e9), unit-tested in
  test_type_conversions.

Defense-in-depth + robustness:
- TraceRouteModule::printRoute: fix an snr_back[-1] OOB read (wrong count in the
  guard) and stop formatting the INT8_MIN "unknown SNR" sentinel as a dB value.
- WaypointModule/NodeDB: sanitize untrusted strings before the OLED renderer and
  the phone-facing ClientNotification (belt-and-suspenders vs PB_VALIDATE_UTF8).
- MeshService::sendToPhone: withhold NODEINFO/WAYPOINT packets whose nested string
  won't cleanly decode, protecting strict phone protobuf decoders without
  affecting mesh relay.

Tests: new test_fuzz_decode (protobuf decode + UTF-8 sanitizer fuzz) and
test_fuzz_packets (perhapsDecode / module-handler / traceroute / phone-gate fuzz),
all under AddressSanitizer; native-suite-count 25 -> 27. Full suite 515/515 green.
This commit is contained in:
Ben Meadors
2026-07-02 16:50:49 -05:00
committed by GitHub
parent 510e9796f9
commit b4dd76a4db
14 changed files with 945 additions and 20 deletions
@@ -210,6 +210,11 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta
return;
}
// Same String(float) stack-overflow guard as EnvironmentTelemetry: form_formaldehyde is the only
// float rendered here, and its raw bytes are unvalidated on decode.
telemetry.variant.air_quality_metrics.form_formaldehyde =
UnitConversions::displaySafeFloat(telemetry.variant.air_quality_metrics.form_formaldehyde);
const auto &m = telemetry.variant.air_quality_metrics;
// Check if any telemetry field has valid data
@@ -375,6 +375,22 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
return;
}
// Bound the float metrics before String(float) renders them (see UnitConversions::displaySafeFloat);
// the stored packet is always the environment variant.
{
auto &e = telemetry.variant.environment_metrics;
e.temperature = UnitConversions::displaySafeFloat(e.temperature);
e.relative_humidity = UnitConversions::displaySafeFloat(e.relative_humidity);
e.barometric_pressure = UnitConversions::displaySafeFloat(e.barometric_pressure);
e.voltage = UnitConversions::displaySafeFloat(e.voltage);
e.current = UnitConversions::displaySafeFloat(e.current);
e.lux = UnitConversions::displaySafeFloat(e.lux);
e.white_lux = UnitConversions::displaySafeFloat(e.white_lux);
e.weight = UnitConversions::displaySafeFloat(e.weight);
e.distance = UnitConversions::displaySafeFloat(e.distance);
e.radiation = UnitConversions::displaySafeFloat(e.radiation);
}
const auto &m = telemetry.variant.environment_metrics;
// Check if any telemetry field has valid data
+11
View File
@@ -1,5 +1,7 @@
#pragma once
#include <cmath>
class UnitConversions
{
public:
@@ -7,4 +9,13 @@ class UnitConversions
static float MetersPerSecondToKnots(float metersPerSecond);
static float MetersPerSecondToMilesPerHour(float metersPerSecond);
static float HectoPascalToInchesOfMercury(float hectoPascal);
// Bound a float before Arduino String(float) renders it: its fixed char[33] + dtostrf overflow
// near FLT_MAX (stack smash). Clamp to +/-1e9 (<=10 digits) and drop non-finite values.
static inline float displaySafeFloat(float v)
{
if (!std::isfinite(v))
return 0.0f;
return v < -1e9f ? -1e9f : (v > 1e9f ? 1e9f : v);
}
};
+1 -1
View File
@@ -452,7 +452,7 @@ void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin,
// If there's a route back (or we are the destination as then the route is complete), print it
if (r->route_back_count > 0 || origin == nodeDB->getNodeNum()) {
route += "\n";
if (r->snr_towards_count > 0 && origin == nodeDB->getNodeNum())
if (origin == nodeDB->getNodeNum() && r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN)
route += vformat("(%.2fdB) 0x%x <-- ", (float)r->snr_back[r->snr_back_count - 1] / 4, origin);
else
route += "...";
+5
View File
@@ -4,6 +4,7 @@
#include "configuration.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/CompassRenderer.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "gps/RTC.h"
@@ -92,6 +93,10 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
return;
}
// Sanitize before these reach the OLED renderer (defense-in-depth vs PB_VALIDATE_UTF8).
sanitizeUtf8(wp.name, sizeof(wp.name));
sanitizeUtf8(wp.description, sizeof(wp.description));
// Get timestamp info. Will pass as a field to drawColumns
char lastStr[20];
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));